From 6eef1c3289806b090e1e45559286a898eb8a9425 Mon Sep 17 00:00:00 2001 From: Hugo Melder Date: Tue, 29 Oct 2024 06:12:34 -0700 Subject: [PATCH] NSKeyValueCoding: Safe-Caching for -[NSObject valueForKey:] (#445) * KVC Caching Implementation * Do not ignore struct name when comparing type encoding as NSPoint and NSSize have the same layout * Use fast-path when using Objective-C 2 * Guard old ValueForKey function when using the fast-path * Add basic NSKeyValueCoding tests * Update Copyright Years * NSKeyValueCoding+Caching: Add Versioning to IVar Slot * safe_caching: Remove Guards * Add type encoding helper header * Rename geometry structs (NSRect, NSPoint, NSSize) for toll-free bridging with CoreGraphics * Move CG struct definitions to CFCGTypes.h * Update known struct encoding prefixes * Windows 64-bit is LLP64 and not LP64 * Re-order to avoid complier warning --------- Co-authored-by: rfm --- Headers/CoreFoundation/CFCGTypes.h | 126 ++++ Headers/Foundation/NSGeometry.h | 25 +- Headers/Foundation/NSObjCRuntime.h | 13 - Source/Additions/GSObjCRuntime.m | 11 +- Source/GNUmakefile | 13 +- Source/Makefile.postamble | 5 + Source/NSKeyValueCoding+Caching.h | 55 ++ Source/NSKeyValueCoding+Caching.m | 639 +++++++++++++++++++ Source/NSKeyValueCoding.m | 17 +- Source/NSValue.m | 17 +- Source/typeEncodingHelper.h | 60 ++ Tests/base/KVC/safe_caching.m | 56 ++ Tests/base/KVC/search_patterns.m | 191 ++++++ Tests/base/KVC/type_encoding.m | 16 + Tests/base/KVC/types.m | 354 ++++++++++ Tests/base/coding/NSArray.1.32bit | Bin 144 -> 149 bytes Tests/base/coding/NSAttributedString.0.32bit | Bin 173 -> 178 bytes Tests/base/coding/NSCharacterSet.0.32bit | Bin 8445 -> 234 bytes Tests/base/coding/NSCharacterSet.0.64bit | Bin 8410 -> 234 bytes Tests/base/coding/NSData.0.32bit | Bin 249 -> 214 bytes Tests/base/coding/NSDate.1.32bit | Bin 161 -> 166 bytes Tests/base/coding/NSDateFormatter.0.32bit | Bin 183 -> 188 bytes Tests/base/coding/NSDateFormatter.0.64bit | Bin 223 -> 188 bytes Tests/base/coding/NSDictionary.0.32bit | Bin 163 -> 168 bytes Tests/base/coding/NSException.0.32bit | Bin 160 -> 225 bytes Tests/base/coding/NSMutableData.0.32bit | Bin 249 -> 214 bytes Tests/base/coding/NSNotification.0.32bit | Bin 185 -> 190 bytes Tests/base/coding/NSNull.0.32bit | Bin 152 -> 157 bytes Tests/base/coding/NSNumber.0.32bit | Bin 176 -> 181 bytes Tests/base/coding/NSNumber.0.64bit | Bin 181 -> 181 bytes Tests/base/coding/NSObject.0.32bit | Bin 139 -> 144 bytes Tests/base/coding/NSSet.0.32bit | Bin 156 -> 161 bytes Tests/base/coding/NSString.1.32bit | Bin 144 -> 149 bytes Tests/base/coding/NSTableView.3.32bit | Bin 857 -> 0 bytes Tests/base/coding/NSURL.0.32bit | Bin 195 -> 223 bytes Tests/base/coding/NSValue.3.32bit | Bin 196 -> 200 bytes Tests/base/coding/NSValue.3.64bit | Bin 201 -> 200 bytes 37 files changed, 1549 insertions(+), 49 deletions(-) create mode 100644 Headers/CoreFoundation/CFCGTypes.h create mode 100644 Source/NSKeyValueCoding+Caching.h create mode 100644 Source/NSKeyValueCoding+Caching.m create mode 100644 Source/typeEncodingHelper.h create mode 100644 Tests/base/KVC/safe_caching.m create mode 100644 Tests/base/KVC/search_patterns.m create mode 100644 Tests/base/KVC/type_encoding.m create mode 100644 Tests/base/KVC/types.m delete mode 100644 Tests/base/coding/NSTableView.3.32bit diff --git a/Headers/CoreFoundation/CFCGTypes.h b/Headers/CoreFoundation/CFCGTypes.h new file mode 100644 index 0000000000..cebc256416 --- /dev/null +++ b/Headers/CoreFoundation/CFCGTypes.h @@ -0,0 +1,126 @@ +/** CFCGTypes.h - CoreFoundation header file for CG types + Copyright (C) 2024 Free Software Foundation, Inc. + + Written by: Hugo Melder + Created: October 2024 + + This file is part of the GNUstep Base Library. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free + Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110 USA. +*/ + +#ifndef _CFCGTypes_h_GNUSTEP_BASE_INCLUDE +#define _CFCGTypes_h_GNUSTEP_BASE_INCLUDE + +#include +#include + +#define CF_DEFINES_CG_TYPES + +#if defined(__has_attribute) && __has_attribute(objc_boxable) +# define CF_BOXABLE __attribute__((objc_boxable)) +#else +# define CF_BOXABLE +#endif + + #if (defined(__LP64__) && __LP64__) || defined(_WIN64) + # define CGFLOAT_TYPE double + # define CGFLOAT_IS_DOUBLE 1 + # define CGFLOAT_MIN DBL_MIN + # define CGFLOAT_MAX DBL_MAX + # define CGFLOAT_EPSILON DBL_EPSILON + #else + # define CGFLOAT_TYPE float + # define CGFLOAT_IS_DOUBLE 0 + # define CGFLOAT_MIN FLT_MIN + # define CGFLOAT_MAX FLT_MAX + # define CGFLOAT_EPSILON FLT_EPSILON + #endif + +typedef CGFLOAT_TYPE CGFloat; +#define CGFLOAT_DEFINED 1 + +struct +CGPoint { + CGFloat x; + CGFloat y; +}; +typedef struct CF_BOXABLE CGPoint CGPoint; + +struct CGSize { + CGFloat width; + CGFloat height; +}; +typedef struct CF_BOXABLE CGSize CGSize; + +#define CGVECTOR_DEFINED 1 + +struct CGVector { + CGFloat dx; + CGFloat dy; +}; +typedef struct CF_BOXABLE CGVector CGVector; + +struct CGRect { + CGPoint origin; + CGSize size; +}; +typedef struct CF_BOXABLE CGRect CGRect; + +enum +{ + CGRectMinXEdge = 0, + CGRectMinYEdge = 1, + CGRectMaxXEdge = 2, + CGRectMaxYEdge = 3 +}; + +typedef struct CGAffineTransform CGAffineTransform; + +struct CGAffineTransform { + CGFloat a, b, c, d; + CGFloat tx, ty; +}; + +#define CF_DEFINES_CGAFFINETRANSFORMCOMPONENTS + +/* |------------------ CGAffineTransformComponents ----------------| + * + * | a b 0 | | sx 0 0 | | 1 0 0 | | cos(t) sin(t) 0 | | 1 0 0 | + * | c d 0 | = | 0 sy 0 | * | sh 1 0 | * |-sin(t) cos(t) 0 | * | 0 1 0 | + * | tx ty 1 | | 0 0 1 | | 0 0 1 | | 0 0 1 | | tx ty 1 | + * CGAffineTransform scale shear rotation translation + */ +typedef struct CGAffineTransformComponents CGAffineTransformComponents; + +struct CGAffineTransformComponents { + + /* Scale factors in X and Y dimensions. Negative values indicate flipping along that axis. */ + CGSize scale; + + /* Shear distortion along the horizontal axis. A value of 0 means no shear. */ + CGFloat horizontalShear; + + /* Rotation angle in radians around the origin. Sign convention may vary + * based on the coordinate system used. */ + CGFloat rotation; + + /* Translation or displacement along the X and Y axes. */ + CGVector translation; +}; + + +#endif // _CFCGTypes_h_GNUSTEP_BASE_INCLUDE diff --git a/Headers/Foundation/NSGeometry.h b/Headers/Foundation/NSGeometry.h index 4bddb843cc..a4bdb99319 100644 --- a/Headers/Foundation/NSGeometry.h +++ b/Headers/Foundation/NSGeometry.h @@ -25,10 +25,12 @@ #ifndef __NSGeometry_h_GNUSTEP_BASE_INCLUDE #define __NSGeometry_h_GNUSTEP_BASE_INCLUDE #import +#import +#ifdef __OBJC__ #import - #import +#endif #if defined(__cplusplus) extern "C" { @@ -56,12 +58,7 @@ extern "C" { CGFloat y; }

Represents a 2-d cartesian position.

*/ -typedef struct _NSPoint NSPoint; -struct _NSPoint -{ - CGFloat x; - CGFloat y; -}; +typedef struct CGPoint NSPoint; #if OS_API_VERSION(GS_API_MACOSX, GS_API_LATEST) /** Array of NSPoint structs. */ @@ -76,12 +73,7 @@ typedef NSPoint *NSPointPointer; CGFloat height; }

Floating point rectangle size.

*/ -typedef struct _NSSize NSSize; -struct _NSSize -{ - CGFloat width; - CGFloat height; -}; +typedef struct CGSize NSSize; #if OS_API_VERSION(GS_API_MACOSX, GS_API_LATEST) /** Array of NSSize structs. */ @@ -97,12 +89,7 @@ typedef NSSize *NSSizePointer; }

Rectangle.

*/ -typedef struct _NSRect NSRect; -struct _NSRect -{ - NSPoint origin; - NSSize size; -}; +typedef struct CGRect NSRect; #if OS_API_VERSION(GS_API_MACOSX, GS_API_LATEST) /** Array of NSRect structs. */ diff --git a/Headers/Foundation/NSObjCRuntime.h b/Headers/Foundation/NSObjCRuntime.h index 2ad90afe2c..ca2abb4be3 100644 --- a/Headers/Foundation/NSObjCRuntime.h +++ b/Headers/Foundation/NSObjCRuntime.h @@ -101,19 +101,6 @@ typedef uintptr_t NSUInteger; # define NSUIntegerMax UINTPTR_MAX #endif /* !defined(NSINTEGER_DEFINED) */ -#if !defined(CGFLOAT_DEFINED) -#if GS_SIZEOF_VOIDP == 8 -#define CGFLOAT_IS_DBL 1 -typedef double CGFloat; -#define CGFLOAT_MIN DBL_MIN -#define CGFLOAT_MAX DBL_MAX -#else -typedef float CGFloat; -#define CGFLOAT_MIN FLT_MIN -#define CGFLOAT_MAX FLT_MAX -#endif -#endif /* !defined(CGFLOAT_DEFINED) */ - #define NSINTEGER_DEFINED 1 #define CGFLOAT_DEFINED 1 #ifndef NS_AUTOMATED_REFCOUNT_UNAVAILABLE diff --git a/Source/Additions/GSObjCRuntime.m b/Source/Additions/GSObjCRuntime.m index 9b63fe2db4..bcbba40189 100644 --- a/Source/Additions/GSObjCRuntime.m +++ b/Source/Additions/GSObjCRuntime.m @@ -48,6 +48,7 @@ #import "../GSPrivate.h" #import "../GSPThread.h" +#import "../typeEncodingHelper.h" #include @@ -1317,7 +1318,8 @@ unsigned long long (*imp)(id, SEL) = break; case _C_STRUCT_B: - if (GSSelectorTypesMatch(@encode(NSPoint), type)) + { + if (IS_CGPOINT_ENCODING(type)) { NSPoint v; @@ -1334,7 +1336,7 @@ unsigned long long (*imp)(id, SEL) = } val = [NSValue valueWithPoint: v]; } - else if (GSSelectorTypesMatch(@encode(NSRange), type)) + else if (IS_NSRANGE_ENCODING(type)) { NSRange v; @@ -1351,7 +1353,7 @@ unsigned long long (*imp)(id, SEL) = } val = [NSValue valueWithRange: v]; } - else if (GSSelectorTypesMatch(@encode(NSRect), type)) + else if (IS_CGRECT_ENCODING(type)) { NSRect v; @@ -1368,7 +1370,7 @@ unsigned long long (*imp)(id, SEL) = } val = [NSValue valueWithRect: v]; } - else if (GSSelectorTypesMatch(@encode(NSSize), type)) + else if (IS_CGSIZE_ENCODING(type)) { NSSize v; @@ -1410,6 +1412,7 @@ unsigned long long (*imp)(id, SEL) = } } break; + } default: #ifdef __GNUSTEP_RUNTIME__ diff --git a/Source/GNUmakefile b/Source/GNUmakefile index d16c9a7536..a2c8e23784 100644 --- a/Source/GNUmakefile +++ b/Source/GNUmakefile @@ -359,6 +359,11 @@ NSZone.m \ externs.m \ objc-load.m +ifeq ($(OBJC_RUNTIME_LIB), ng) + BASE_MFILES += \ + NSKeyValueCoding+Caching.m +endif + ifneq ($(GNUSTEP_TARGET_OS), mingw32) ifneq ($(GNUSTEP_TARGET_OS), mingw64) ifneq ($(GNUSTEP_TARGET_OS), windows) @@ -414,6 +419,11 @@ win32-load.h \ NSCallBacks.h \ tzfile.h +# Definitions for toll-free bridging of known structures +# such as NSRect, NSPoint, or NSSize. +COREFOUNDATION_HEADERS = \ +CFCGTypes.h + FOUNDATION_HEADERS = \ Foundation.h \ FoundationErrors.h \ @@ -586,7 +596,8 @@ NSZone.h HEADERS_INSTALL = \ $(OBJECTIVEC2_HEADERS) \ $(GNUSTEPBASE_HEADERS) \ - $(FOUNDATION_HEADERS) + $(FOUNDATION_HEADERS) \ + $(COREFOUNDATION_HEADERS) GENERATED_HFILES = \ dynamic-load.h \ diff --git a/Source/Makefile.postamble b/Source/Makefile.postamble index 0efe23cafc..dc09bd72eb 100644 --- a/Source/Makefile.postamble +++ b/Source/Makefile.postamble @@ -56,6 +56,11 @@ after-install:: done endif after-install:: + $(MKDIRS) $(GNUSTEP_HEADERS)/CoreFoundation + for file in $(COREFOUNDATION_HEADERS); do \ + $(INSTALL_DATA) ../Headers/CoreFoundation/$$file \ + $(GNUSTEP_HEADERS)/CoreFoundation/$$file ; \ + done $(MKDIRS) $(GNUSTEP_HEADERS)/GNUstepBase for file in $(GNUSTEPBASE_HEADERS); do \ $(INSTALL_DATA) ../Headers/GNUstepBase/$$file \ diff --git a/Source/NSKeyValueCoding+Caching.h b/Source/NSKeyValueCoding+Caching.h new file mode 100644 index 0000000000..f5c45fdf4d --- /dev/null +++ b/Source/NSKeyValueCoding+Caching.h @@ -0,0 +1,55 @@ +/** Key-Value Coding Safe Caching Support + Copyright (C) 2024 Free Software Foundation, Inc. + + Written by: Hugo Melder + Created: August 2024 + + This file is part of the GNUstep Base Library. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free + Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110 USA. +*/ + +/** + * It turns out that valueForKey: is a very expensive operation, and a major + * bottleneck for Key-Value Observing and other operations such as sorting + * an array by key. + * + * The accessor search patterns for Key-Value observing are discussed in the + * Apple Key-Value Coding Programming Guide. The return value may be + * encapuslated into an NSNumber or NSValue object, depending on the Objective-C + * type encoding of the return value. This means that once valueForKey: found an + * existing accessor, the Objective-C type encoding of the accessor is + * retrieved. We then go through a huge switch case to determine the right way + * to invoke the IMP and potentially encapsulate the return type. The resulting + * object is then returned. + * The algorithm for setValue:ForKey: is similar. + * + * We can speed this up by caching the IMP of the accessor in a hash table. + * However, without proper versioning, this quickly becomes very dangerous. + * The user might exchange implementations, or add new ones expecting the + * search pattern invariant to still hold. If we clamp onto an IMP, this + * invariant no longer holds. + * + * We will make use of libobjc2's safe caching to avoid this. + * + * Note that the caching is opaque. You will only need to redirect all + * valueForKey: calls to the function below. + */ + +#import "Foundation/NSString.h" + +id +valueForKeyWithCaching(id obj, NSString *aKey); \ No newline at end of file diff --git a/Source/NSKeyValueCoding+Caching.m b/Source/NSKeyValueCoding+Caching.m new file mode 100644 index 0000000000..07dc3ec39b --- /dev/null +++ b/Source/NSKeyValueCoding+Caching.m @@ -0,0 +1,639 @@ +/** Key-Value Coding Safe Caching Support. + Copyright (C) 2024 Free Software Foundation, Inc. + + Written by: Hugo Melder + Created: August 2024 + + This file is part of the GNUstep Base Library. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free + Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110 USA. +*/ + +#import +#import +#import + +#import "common.h" // for likely and unlikely +#import "typeEncodingHelper.h" +#import "Foundation/NSKeyValueCoding.h" +#import "Foundation/NSMethodSignature.h" +#import "Foundation/NSValue.h" +#import "Foundation/NSInvocation.h" +#import "NSKeyValueCoding+Caching.h" + + +struct _KVCCacheSlot +{ + Class cls; + SEL selector; + const char *types; + uintptr_t hash; + // The slot version returned by objc_get_slot2. + // Set to zero when this is caching an ivar lookup + uint64_t version; + // If selector is zero, we cache the ivar offset, + // otherwise the IMP of the accessor. + // Use the corresponding get functions below. + union + { + IMP imp; + intptr_t offset; + // Just for readability when checking for emptyness + intptr_t contents; + }; + id (*get)(struct _KVCCacheSlot *, id); +}; + +static inline uintptr_t +_KVCCacheSlotHash(const void *ptr) +{ + struct _KVCCacheSlot *a = (struct _KVCCacheSlot *) ptr; + return (uintptr_t) a->cls ^ (uintptr_t) a->hash; +} + +static inline BOOL +_KVCCacheSlotEqual(const void *ptr1, const void *ptr2) +{ + struct _KVCCacheSlot *a = (struct _KVCCacheSlot *) ptr1; + struct _KVCCacheSlot *b = (struct _KVCCacheSlot *) ptr2; + + return a->cls == b->cls && a->hash == b->hash; +} + +void inline _KVCCacheSlotRelease(const void *ptr) +{ + free((struct _KVCCacheSlot *) ptr); +} + +// We only need a hash table not a map +#define GSI_MAP_HAS_VALUE 0 +#define GSI_MAP_RETAIN_KEY(M, X) +#define GSI_MAP_RELEASE_KEY(M, X) (_KVCCacheSlotRelease(X.ptr)) +#define GSI_MAP_HASH(M, X) (_KVCCacheSlotHash(X.ptr)) +#define GSI_MAP_EQUAL(M, X, Y) (_KVCCacheSlotEqual(X.ptr, Y.ptr)) +#define GSI_MAP_KTYPES GSUNION_PTR +#import "GNUstepBase/GSIMap.h" +#import "GSPThread.h" + +/* + * Templating for poor people: + * We need to call IMP with the correct function signature and box + * the return value accordingly. + */ +#define KVC_CACHE_FUNC(_type, _fnname, _cls, _meth) \ + static id _fnname(struct _KVCCacheSlot *slot, id obj) \ + { \ + _type val = ((_type(*)(id, SEL)) slot->imp)(obj, slot->selector); \ + return [_cls _meth:val]; \ + } +#define KVC_CACHE_IVAR_FUNC(_type, _fnname, _cls, _meth) \ + static id _fnname##ForIvar(struct _KVCCacheSlot *slot, id obj) \ + { \ + _type val = *(_type *) ((char *) obj + slot->offset); \ + return [_cls _meth:val]; \ + } + +#define CACHE_NSNUMBER_GEN_FUNCS(_type, _fnname, _numberMethName) \ + KVC_CACHE_FUNC(_type, _fnname, NSNumber, numberWith##_numberMethName) \ + KVC_CACHE_IVAR_FUNC(_type, _fnname, NSNumber, numberWith##_numberMethName) + +#define CACHE_NSVALUE_GEN_FUNCS(_type, _fnname, _valueMethName) \ + KVC_CACHE_FUNC(_type, _fnname, NSValue, valueWith##_valueMethName) \ + KVC_CACHE_IVAR_FUNC(_type, _fnname, NSValue, valueWith##_valueMethName) + +// Ignore the alignment warning when casting the obj + offset address +// to the proper type. +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wcast-align" + +CACHE_NSNUMBER_GEN_FUNCS(char, _getBoxedChar, Char); +CACHE_NSNUMBER_GEN_FUNCS(int, _getBoxedInt, Int); +CACHE_NSNUMBER_GEN_FUNCS(short, _getBoxedShort, Short); +CACHE_NSNUMBER_GEN_FUNCS(long, _getBoxedLong, Long); +CACHE_NSNUMBER_GEN_FUNCS(long long, _getBoxedLongLong, LongLong); +CACHE_NSNUMBER_GEN_FUNCS(unsigned char, _getBoxedUnsignedChar, UnsignedChar); +CACHE_NSNUMBER_GEN_FUNCS(unsigned int, _getBoxedUnsignedInt, UnsignedInt); +CACHE_NSNUMBER_GEN_FUNCS(unsigned short, _getBoxedUnsignedShort, UnsignedShort); +CACHE_NSNUMBER_GEN_FUNCS(unsigned long, _getBoxedUnsignedLong, UnsignedLong); +CACHE_NSNUMBER_GEN_FUNCS(unsigned long long, _getBoxedUnsignedLongLong, + UnsignedLongLong); +CACHE_NSNUMBER_GEN_FUNCS(float, _getBoxedFloat, Float); +CACHE_NSNUMBER_GEN_FUNCS(double, _getBoxedDouble, Double); +CACHE_NSNUMBER_GEN_FUNCS(bool, _getBoxedBool, Bool); + +CACHE_NSVALUE_GEN_FUNCS(NSPoint, _getBoxedNSPoint, Point); +CACHE_NSVALUE_GEN_FUNCS(NSRange, _getBoxedNSRange, Range); +CACHE_NSVALUE_GEN_FUNCS(NSRect, _getBoxedNSRect, Rect); +CACHE_NSVALUE_GEN_FUNCS(NSSize, _getBoxedNSSize, Size); + +static id +_getBoxedId(struct _KVCCacheSlot *slot, id obj) +{ + id val = ((id(*)(id, SEL)) slot->imp)(obj, slot->selector); + return val; +} +static id +_getBoxedIdForIvar(struct _KVCCacheSlot *slot, id obj) +{ + id val = *(id *) ((char *) obj + slot->offset); + return val; +} +static id +_getBoxedClass(struct _KVCCacheSlot *slot, id obj) +{ + Class val = ((Class(*)(id, SEL)) slot->imp)(obj, slot->selector); + return val; +} +static id +_getBoxedClassForIvar(struct _KVCCacheSlot *slot, id obj) +{ + Class val = *(Class *) ((char *) obj + slot->offset); + return val; +} + +// TODO: This can be optimised and is still very expensive +static id +_getBoxedStruct(struct _KVCCacheSlot *slot, id obj) +{ + NSInvocation *inv; + const char *types = slot->types; + NSMethodSignature *sig = [NSMethodSignature signatureWithObjCTypes: types]; + size_t retSize = [sig methodReturnLength]; + char ret[retSize]; + + inv = [NSInvocation invocationWithMethodSignature: sig]; + [inv setSelector: slot->selector]; + [inv invokeWithTarget: obj]; + [inv getReturnValue: ret]; + + return [NSValue valueWithBytes:ret objCType:[sig methodReturnType]]; +} +static id +_getBoxedStructForIvar(struct _KVCCacheSlot *slot, id obj) +{ + const char *end = objc_skip_typespec(slot->types); + size_t length = end - slot->types; + char returnType[length + 1]; + memcpy(returnType, slot->types, length); + returnType[length] = '\0'; + + return [NSValue valueWithBytes:((char *) obj + slot->offset) + objCType:returnType]; +} + +#pragma clang diagnostic pop + +static struct _KVCCacheSlot +_getBoxedBlockForIVar(NSString *key, Ivar ivar) +{ + const char *encoding = ivar_getTypeEncoding(ivar); + struct _KVCCacheSlot slot = {}; + // Return a zeroed out slot. It is the caller's responsibility to call + // valueForUndefinedKey: + if (unlikely(encoding == NULL)) + { + return slot; + } + + slot.offset = ivar_getOffset(ivar); + slot.types = encoding; + // Get the current objc_method_cache_version as we do not explicitly + // request a new slot when looking up ivars. + slot.version = objc_method_cache_version; + + switch (encoding[0]) + { + case '@': { + slot.get = _getBoxedIdForIvar; + return slot; + } + case 'B': { + slot.get = _getBoxedBoolForIvar; + return slot; + } + case 'l': { + slot.get = _getBoxedLongForIvar; + return slot; + } + case 'f': { + slot.get = _getBoxedFloatForIvar; + return slot; + } + case 'd': { + slot.get = _getBoxedDoubleForIvar; + return slot; + } + case 'i': { + slot.get = _getBoxedIntForIvar; + return slot; + } + case 'I': { + slot.get = _getBoxedUnsignedIntForIvar; + return slot; + } + case 'L': { + slot.get = _getBoxedUnsignedLongForIvar; + return slot; + } + case 'q': { + slot.get = _getBoxedLongLongForIvar; + return slot; + } + case 'Q': { + slot.get = _getBoxedUnsignedLongLongForIvar; + return slot; + } + case 'c': { + slot.get = _getBoxedCharForIvar; + return slot; + } + case 's': { + slot.get = _getBoxedShortForIvar; + return slot; + } + case 'C': { + slot.get = _getBoxedUnsignedCharForIvar; + return slot; + } + case 'S': { + slot.get = _getBoxedUnsignedShortForIvar; + return slot; + } + case '#': { + slot.get = _getBoxedClassForIvar; + return slot; + } + case '{': { + if (IS_NSRANGE_ENCODING(encoding)) + { + slot.get = _getBoxedNSRangeForIvar; + return slot; + } + else if (IS_CGRECT_ENCODING(encoding)) + { + slot.get = _getBoxedNSRectForIvar; + return slot; + } + else if (IS_CGPOINT_ENCODING(encoding)) + { + slot.get = _getBoxedNSPointForIvar; + return slot; + } + else if (IS_CGSIZE_ENCODING(encoding)) + { + slot.get = _getBoxedNSSizeForIvar; + return slot; + } + + slot.get = _getBoxedStructForIvar; + return slot; + } + default: + slot.contents = 0; + return slot; + } +} + +static struct _KVCCacheSlot +_getBoxedBlockForMethod(NSString *key, Method method, SEL sel, uint64_t version) +{ + const char *encoding = method_getTypeEncoding(method); + struct _KVCCacheSlot slot = {}; + if (unlikely(encoding == NULL)) + { + // Return a zeroed out slot. It is the caller's responsibility to call + // valueForUndefinedKey: or parse unknown structs (when type encoding + // starts with '{') + return slot; + } + + slot.imp = method_getImplementation(method); + slot.selector = sel; + slot.types = encoding; + slot.version = version; + + // TODO: Move most commonly used types up the switch statement + switch (encoding[0]) + { + case '@': { + slot.get = _getBoxedId; + return slot; + } + case 'B': { + slot.get = _getBoxedBool; + return slot; + } + case 'l': { + slot.get = _getBoxedLong; + return slot; + } + case 'f': { + slot.get = _getBoxedFloat; + return slot; + } + case 'd': { + slot.get = _getBoxedDouble; + return slot; + } + case 'i': { + slot.get = _getBoxedInt; + return slot; + } + case 'I': { + slot.get = _getBoxedUnsignedInt; + return slot; + } + case 'L': { + slot.get = _getBoxedUnsignedLong; + return slot; + } + case 'q': { + slot.get = _getBoxedLongLong; + return slot; + } + case 'Q': { + slot.get = _getBoxedUnsignedLongLong; + return slot; + } + case 'c': { + slot.get = _getBoxedChar; + return slot; + } + case 's': { + slot.get = _getBoxedShort; + return slot; + } + case 'C': { + slot.get = _getBoxedUnsignedChar; + return slot; + } + case 'S': { + slot.get = _getBoxedUnsignedShort; + return slot; + } + case '#': { + slot.get = _getBoxedClass; + return slot; + } + case '{': { + if (IS_NSRANGE_ENCODING(encoding)) + { + slot.get = _getBoxedNSRange; + return slot; + } + else if (IS_CGRECT_ENCODING(encoding)) + { + slot.get = _getBoxedNSRect; + return slot; + } + else if (IS_CGPOINT_ENCODING(encoding)) + { + slot.get = _getBoxedNSPoint; + return slot; + } + else if (IS_CGSIZE_ENCODING(encoding)) + { + slot.get = _getBoxedNSSize; + return slot; + } + + slot.get = _getBoxedStruct; + return slot; + } + default: + slot.contents = 0; + return slot; + } +} + +// libobjc2 does not offer an API for recursively looking up a slot with +// just the class and a selector. +// The behaviour of this function is similar to that of class_getInstanceMethod +// and recurses into the super classes. Additionally, we ask the class, if it +// can resolve the instance method dynamically by calling -[NSObject +// resolveInstanceMethod:]. +// +// objc_slot2 has the same struct layout as objc_method. +Method _Nullable _class_getMethodRecursive(Class aClass, SEL aSelector, + uint64_t *version) +{ + struct objc_slot2 *slot; + + if (0 == aClass) + { + return NULL; + } + + if (0 == aSelector) + { + return NULL; + } + + // Do a dtable lookup to find out which class the method comes from. + slot = objc_get_slot2(aClass, aSelector, version); + if (NULL != slot) + { + return (Method) slot; + } + + // Ask if class is able to dynamically register this method + if ([aClass resolveInstanceMethod:aSelector]) + { + return (Method) _class_getMethodRecursive(aClass, aSelector, version); + } + + // Recurse into super classes + return (Method) _class_getMethodRecursive(class_getSuperclass(aClass), + aSelector, version); +} + +static struct _KVCCacheSlot +ValueForKeyLookup(Class cls, NSObject *self, NSString *boxedKey, + const char *key, unsigned size) +{ + const char *name; + char buf[size + 5]; + char lo; + char hi; + SEL sel = 0; + Method meth = NULL; + uint64_t version = 0; + struct _KVCCacheSlot slot = {}; + + if (unlikely(size == 0)) + { + return slot; + } + + memcpy(buf, "_get", 4); + memcpy(&buf[4], key, size); + buf[size + 4] = '\0'; + lo = buf[4]; + hi = islower(lo) ? toupper(lo) : lo; + buf[4] = hi; + + // 1.1 Check if the _get accessor method exists + name = &buf[1]; // getKey + sel = sel_registerName(name); + if ((meth = _class_getMethodRecursive(cls, sel, &version)) != NULL) + { + return _getBoxedBlockForMethod(boxedKey, meth, sel, version); + } + + // 1.2 Check if the accessor method exists + buf[4] = lo; + name = &buf[4]; // key + sel = sel_registerName(name); + if ((meth = _class_getMethodRecursive(cls, sel, &version)) != NULL) + { + return _getBoxedBlockForMethod(boxedKey, meth, sel, version); + } + + // 1.3 Check if the is accessor method exists + buf[2] = 'i'; + buf[3] = 's'; + buf[4] = hi; + name = &buf[2]; // isKey + sel = sel_registerName(name); + if ((meth = _class_getMethodRecursive(cls, sel, &version)) != NULL) + { + return _getBoxedBlockForMethod(boxedKey, meth, sel, version); + } + + // 1.4 Check if the _ accessor method exists. Otherwise check + // if we are allowed to access the instance variables directly. + buf[3] = '_'; + buf[4] = lo; + name = &buf[3]; // _key + sel = sel_registerName(name); + if ((meth = _class_getMethodRecursive(cls, sel, &version)) != NULL) + { + return _getBoxedBlockForMethod(boxedKey, meth, sel, version); + } + + // Step 2. and 3. (NSArray and NSSet accessors) are implemented + // in the respective classes. + + // 4. Last try: Ivar access + if ([cls accessInstanceVariablesDirectly] == YES) + { + // 4.1 Check if the _ ivar exists + Ivar ivar = class_getInstanceVariable(cls, name); + if (ivar != NULL) + { // _key + return _getBoxedBlockForIVar(boxedKey, ivar); + } + + // 4.2 Check if the _is ivar exists + buf[1] = '_'; + buf[2] = 'i'; + buf[3] = 's'; + buf[4] = hi; + name = &buf[1]; // _isKey + ivar = class_getInstanceVariable(cls, name); + if (ivar != NULL) + { + return _getBoxedBlockForIVar(boxedKey, ivar); + } + + // 4.3 Check if the ivar exists + buf[4] = lo; + name = &buf[4]; // key + ivar = class_getInstanceVariable(cls, name); + if (ivar != NULL) + { + return _getBoxedBlockForIVar(boxedKey, ivar); + } + + // 4.4 Check if the is ivar exists + buf[4] = hi; + name = &buf[2]; // isKey + ivar = class_getInstanceVariable(cls, name); + if (ivar != NULL) + { + return _getBoxedBlockForIVar(boxedKey, ivar); + } + } + + return slot; +} + +id +valueForKeyWithCaching(id obj, NSString *aKey) +{ + struct _KVCCacheSlot *cachedSlot = NULL; + GSIMapNode node = NULL; + static GSIMapTable_t cacheTable = {}; + static gs_mutex_t cacheTableLock = GS_MUTEX_INIT_STATIC; + + Class cls = object_getClass(obj); + // Fill out the required fields for hashing + struct _KVCCacheSlot slot = {.cls = cls, .hash = [aKey hash]}; + + GS_MUTEX_LOCK(cacheTableLock); + if (cacheTable.zone == 0) + { + // TODO: Tweak initial capacity + GSIMapInitWithZoneAndCapacity(&cacheTable, NSDefaultMallocZone(), 64); + } + node = GSIMapNodeForKey(&cacheTable, (GSIMapKey) (void *) &slot); + GS_MUTEX_UNLOCK(cacheTableLock); + + if (node == NULL) + { + // Lookup the getter + slot + = ValueForKeyLookup(cls, obj, aKey, [aKey UTF8String], [aKey length]); + if (slot.contents != 0) + { + slot.cls = cls; + slot.hash = [aKey hash]; + + // Copy slot to heap + cachedSlot + = (struct _KVCCacheSlot *) malloc(sizeof(struct _KVCCacheSlot)); + memcpy(cachedSlot, &slot, sizeof(struct _KVCCacheSlot)); + + GS_MUTEX_LOCK(cacheTableLock); + node = GSIMapAddKey(&cacheTable, (GSIMapKey) (void *) cachedSlot); + GS_MUTEX_UNLOCK(cacheTableLock); + } + else + { + return [obj valueForUndefinedKey:aKey]; + } + } + cachedSlot = node->key.ptr; + + // Check if a new method was registered. If this is the case, + // the objc_method_cache_version was incremented and we need to update the + // cache. + if (objc_method_cache_version != cachedSlot->version) + { + // Lookup the getter + // TODO: We can optimise this by supplying a hint (return type etc.) + // as it is unlikely, that the return type has changed. + slot + = ValueForKeyLookup(cls, obj, aKey, [aKey UTF8String], [aKey length]); + + // Update entry + GS_MUTEX_LOCK(cacheTableLock); + memcpy(cachedSlot, &slot, sizeof(struct _KVCCacheSlot)); + GS_MUTEX_UNLOCK(cacheTableLock); + } + + return cachedSlot->get(cachedSlot, obj); +} diff --git a/Source/NSKeyValueCoding.m b/Source/NSKeyValueCoding.m index 05e73917e5..8e648d62c4 100644 --- a/Source/NSKeyValueCoding.m +++ b/Source/NSKeyValueCoding.m @@ -41,6 +41,9 @@ #include "NSKeyValueMutableArray.m" #include "NSKeyValueMutableSet.m" +#if defined(__OBJC2__) +#import "NSKeyValueCoding+Caching.h" +#endif /* this should move into autoconf once it's accepted */ #define WANT_DEPRECATED_KVC_COMPAT 1 @@ -144,6 +147,7 @@ static inline void setupCompat() GSObjCSetVal(self, key, anObject, sel, type, size, off); } +#if !defined(__OBJC2__) static id ValueForKey(NSObject *self, const char *key, unsigned size) { SEL sel = 0; @@ -166,12 +170,12 @@ static id ValueForKey(NSObject *self, const char *key, unsigned size) name = &buf[1]; // getKey sel = sel_getUid(name); - if (sel == 0 || [self respondsToSelector: sel] == NO) + if ([self respondsToSelector: sel] == NO) { buf[4] = lo; name = &buf[4]; // key sel = sel_getUid(name); - if (sel == 0 || [self respondsToSelector: sel] == NO) + if ([self respondsToSelector: sel] == NO) { buf[4] = hi; buf[3] = 's'; @@ -190,13 +194,13 @@ static id ValueForKey(NSObject *self, const char *key, unsigned size) buf[4] = hi; name = buf; // _getKey sel = sel_getUid(name); - if (sel == 0 || [self respondsToSelector: sel] == NO) + if ([self respondsToSelector: sel] == NO) { buf[4] = lo; buf[3] = '_'; name = &buf[3]; // _key sel = sel_getUid(name); - if (sel == 0 || [self respondsToSelector: sel] == NO) + if ([self respondsToSelector: sel] == NO) { sel = 0; } @@ -229,6 +233,7 @@ static id ValueForKey(NSObject *self, const char *key, unsigned size) } return GSObjCGetVal(self, key, sel, type, size, off); } +#endif @implementation NSObject(KeyValueCoding) @@ -513,6 +518,9 @@ - (BOOL) validateValue: (id*)aValue - (id) valueForKey: (NSString*)aKey { + #if defined(__OBJC2__) + return valueForKeyWithCaching(self, aKey); + #else unsigned size = [aKey length] * 8; char key[size + 1]; @@ -521,6 +529,7 @@ - (id) valueForKey: (NSString*)aKey encoding: NSUTF8StringEncoding]; size = strlen(key); return ValueForKey(self, key, size); + #endif } diff --git a/Source/NSValue.m b/Source/NSValue.m index a19aee6bef..4dc85f1503 100644 --- a/Source/NSValue.m +++ b/Source/NSValue.m @@ -28,6 +28,7 @@ */ #import "common.h" +#import "typeEncodingHelper.h" #import "Foundation/NSValue.h" #import "Foundation/NSCoder.h" #import "Foundation/NSDictionary.h" @@ -411,28 +412,28 @@ - (void) encodeWithCoder: (NSCoder *)coder size = strlen(objctype)+1; [coder encodeValueOfObjCType: @encode(unsigned) at: &size]; [coder encodeArrayOfObjCType: @encode(signed char) count: size at: objctype]; - if (strncmp("{_NSSize=", objctype, 9) == 0) + if (strncmp(CGSIZE_ENCODING_PREFIX, objctype, strlen(CGSIZE_ENCODING_PREFIX)) == 0) { NSSize v = [self sizeValue]; [coder encodeValueOfObjCType: objctype at: &v]; return; } - else if (strncmp("{_NSPoint=", objctype, 10) == 0) + else if (strncmp(CGPOINT_ENCODING_PREFIX, objctype, strlen(CGPOINT_ENCODING_PREFIX)) == 0) { NSPoint v = [self pointValue]; [coder encodeValueOfObjCType: objctype at: &v]; return; } - else if (strncmp("{_NSRect=", objctype, 9) == 0) + else if (strncmp(CGRECT_ENCODING_PREFIX, objctype, strlen(CGRECT_ENCODING_PREFIX)) == 0) { NSRect v = [self rectValue]; [coder encodeValueOfObjCType: objctype at: &v]; return; } - else if (strncmp("{_NSRange=", objctype, 10) == 0) + else if (strncmp(NSRANGE_ENCODING_PREFIX, objctype, strlen(NSRANGE_ENCODING_PREFIX)) == 0) { NSRange v = [self rangeValue]; @@ -480,13 +481,13 @@ - (id) initWithCoder: (NSCoder *)coder [coder decodeArrayOfObjCType: @encode(signed char) count: size at: (void*)objctype]; - if (strncmp("{_NSSize=", objctype, 9) == 0) + if (strncmp(CGSIZE_ENCODING_PREFIX, objctype, strlen(CGSIZE_ENCODING_PREFIX)) == 0) c = [abstractClass valueClassWithObjCType: @encode(NSSize)]; - else if (strncmp("{_NSPoint=", objctype, 10) == 0) + else if (strncmp(CGPOINT_ENCODING_PREFIX, objctype, strlen(CGPOINT_ENCODING_PREFIX)) == 0) c = [abstractClass valueClassWithObjCType: @encode(NSPoint)]; - else if (strncmp("{_NSRect=", objctype, 9) == 0) + else if (strncmp(CGRECT_ENCODING_PREFIX, objctype, strlen(CGRECT_ENCODING_PREFIX)) == 0) c = [abstractClass valueClassWithObjCType: @encode(NSRect)]; - else if (strncmp("{_NSRange=", objctype, 10) == 0) + else if (strncmp(NSRANGE_ENCODING_PREFIX, objctype, strlen(NSRANGE_ENCODING_PREFIX)) == 0) c = [abstractClass valueClassWithObjCType: @encode(NSRange)]; else c = [abstractClass valueClassWithObjCType: objctype]; diff --git a/Source/typeEncodingHelper.h b/Source/typeEncodingHelper.h new file mode 100644 index 0000000000..9fc95cdb8d --- /dev/null +++ b/Source/typeEncodingHelper.h @@ -0,0 +1,60 @@ +/** Type-Encoding Helper + Copyright (C) 2024 Free Software Foundation, Inc. + + Written by: Hugo Melder + Created: August 2024 + + This file is part of the GNUstep Base Library. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free + Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110 USA. +*/ + +#ifndef __TYPE_ENCODING_HELPER_H +#define __TYPE_ENCODING_HELPER_H + +/* +* Type-encoding for known structs in Foundation and CoreGraphics. +* From macOS 14.4.1 23E224 arm64: + +* @encoding(NSRect) -> {CGRect={CGPoint=dd}{CGSize=dd}} +* @encoding(CGRect) -> {CGRect={CGPoint=dd}{CGSize=dd}} +* @encoding(NSPoint) -> {CGPoint=dd} +* @encoding(CGPoint) -> {CGPoint=dd} +* @encoding(NSSize) -> {CGSize=dd} +* @encoding(CGSize) -> {CGSize=dd} +* @encoding(NSRange) -> {_NSRange=QQ} +* @encoding(CFRange) -> {?=qq} +* +* Note that NSRange and CFRange are not toll-free bridged. +* You cannot pass a CFRange to +[NSValue valueWithRange:] +* as type encoding is different. +* +* We cannot enforce this using static asserts, as @encode +* is not a constexpr. It is therefore checked in +* Tests/base/KVC/type_encoding.m +*/ + +static const char *CGPOINT_ENCODING_PREFIX = "{CGPoint="; +static const char *CGSIZE_ENCODING_PREFIX = "{CGSize="; +static const char *CGRECT_ENCODING_PREFIX = "{CGRect="; +static const char *NSRANGE_ENCODING_PREFIX = "{_NSRange="; + +#define IS_CGPOINT_ENCODING(encoding) (strncmp(encoding, CGPOINT_ENCODING_PREFIX, strlen(CGPOINT_ENCODING_PREFIX)) == 0) +#define IS_CGSIZE_ENCODING(encoding) (strncmp(encoding, CGSIZE_ENCODING_PREFIX, strlen(CGSIZE_ENCODING_PREFIX)) == 0) +#define IS_CGRECT_ENCODING(encoding) (strncmp(encoding, CGRECT_ENCODING_PREFIX, strlen(CGRECT_ENCODING_PREFIX)) == 0) +#define IS_NSRANGE_ENCODING(encoding) (strncmp(encoding, NSRANGE_ENCODING_PREFIX, strlen(NSRANGE_ENCODING_PREFIX)) == 0) + +#endif /* __TYPE_ENCODING_HELPER_H */ diff --git a/Tests/base/KVC/safe_caching.m b/Tests/base/KVC/safe_caching.m new file mode 100644 index 0000000000..41a7dc8a08 --- /dev/null +++ b/Tests/base/KVC/safe_caching.m @@ -0,0 +1,56 @@ +#import +#import +#import + +#import "Testing.h" + +@interface TestClass : NSObject + +- (float)floatVal; +@end + +@implementation TestClass + +- (float)floatVal +{ + return 1.0f; +} +@end + +static float +getFloatValIMP(id receiver, SEL cmd) +{ + return 2.0f; +} + +void +testInstallingNewMethodAfterCaching(void) +{ + TestClass *obj = [TestClass new]; + + START_SET("Installing Methods after initial Cache") + + // Initial lookups + PASS_EQUAL([obj valueForKey:@"floatVal"], [NSNumber numberWithFloat:1.0f], + "Initial lookup has the correct value"); + // Slots are now cached + + // Register getFloatVal which should be used if available according to search + // pattern + SEL sel = sel_registerName("getFloatVal"); + class_addMethod([TestClass class], sel, (IMP) getFloatValIMP, "f@:"); + + PASS_EQUAL([obj valueForKey:@"floatVal"], [NSNumber numberWithFloat:2.0f], + "Slot was correctly invalidated"); + + END_SET("Installing Methods after initial Cache") + + [obj release]; +} + +int +main(int argc, char *argv[]) +{ + testInstallingNewMethodAfterCaching(); + return 0; +} diff --git a/Tests/base/KVC/search_patterns.m b/Tests/base/KVC/search_patterns.m new file mode 100644 index 0000000000..1eb4d5f2e5 --- /dev/null +++ b/Tests/base/KVC/search_patterns.m @@ -0,0 +1,191 @@ +#import +#import + +#import "Testing.h" + +// For ivars: _, _is, , or is, in that order. +// For methods: get, , is, or _ in that order. +@interface SearchOrder : NSObject +{ + long long _longLong; + long long _isLongLong; + long long longLong; + long long isLongLong; + + unsigned char _isUnsignedChar; + unsigned char unsignedChar; + unsigned char isUnsignedChar; + + unsigned int unsignedInt; + unsigned int isUnsignedInt; + + unsigned long isUnsignedLong; +} + +- (instancetype)init; + +- (signed char)getChar; +- (signed char)char; +- (signed char)isChar; +- (signed char)_char; + +- (int)int; +- (int)isInt; +- (int)_int; + +- (short)isShort; +- (short)_short; + +- (long)_long; + +@end + +@implementation SearchOrder + +- (instancetype)init +{ + self = [super init]; + + if (self) + { + _longLong = LLONG_MAX; + _isLongLong = LLONG_MAX - 1; + longLong = LLONG_MAX - 2; + isLongLong = LLONG_MAX - 3; + + _isUnsignedChar = UCHAR_MAX; + unsignedChar = UCHAR_MAX - 1; + isUnsignedChar = UCHAR_MAX - 2; + + unsignedInt = UINT_MAX; + isUnsignedInt = UINT_MAX - 1; + + isUnsignedLong = ULONG_MAX; + } + + return self; +} + +- (signed char)getChar +{ + return SCHAR_MAX; +} +- (signed char)char +{ + return SCHAR_MAX - 1; +} +- (signed char)isChar +{ + return SCHAR_MAX - 2; +} +- (signed char)_char +{ + return SCHAR_MAX - 3; +} + +- (int)int +{ + return INT_MAX; +} +- (int)isInt +{ + return INT_MAX - 1; +} +- (int)_int +{ + return INT_MAX - 2; +} + +- (short)isShort +{ + return SHRT_MAX; +} +- (short)_short +{ + return SHRT_MAX - 1; +} + +- (long)_long +{ + return LONG_MAX; +} + +@end + +@interface SearchOrderNoIvarAccess : NSObject +{ + bool _boolVal; + bool _isBoolVal; + bool boolVal; + bool isBoolVal; +} + +@end + +@implementation SearchOrderNoIvarAccess + ++ (BOOL)accessInstanceVariablesDirectly +{ + return NO; +} + +@end + +static void +testSearchOrder(void) +{ + SearchOrder *so = [SearchOrder new]; + + START_SET("Search Order"); + + PASS_EQUAL([so valueForKey:@"char"], [NSNumber numberWithChar:SCHAR_MAX], + "get is used when available"); + PASS_EQUAL([so valueForKey:@"int"], [NSNumber numberWithInt:INT_MAX], + " is used when get is not available"); + PASS_EQUAL([so valueForKey:@"short"], [NSNumber numberWithShort:SHRT_MAX], + "is is used when get and is not available"); + PASS_EQUAL( + [so valueForKey:@"long"], [NSNumber numberWithLong:LONG_MAX], + "_ is used when get, , and is is not available"); + PASS_EQUAL( + [so valueForKey:@"longLong"], [NSNumber numberWithLongLong:LLONG_MAX], + "_ ivar is used when get, , and is is not available"); + PASS_EQUAL( + [so valueForKey:@"unsignedChar"], + [NSNumber numberWithUnsignedChar:UCHAR_MAX], + "_is ivar is used when get, , and is is not available"); + PASS_EQUAL( + [so valueForKey:@"unsignedInt"], [NSNumber numberWithUnsignedInt:UINT_MAX], + " ivar is used when get, , and is is not available"); + PASS_EQUAL( + [so valueForKey:@"unsignedLong"], + [NSNumber numberWithUnsignedLong:ULONG_MAX], + "is ivar is used when get, , and is is not available"); + + END_SET("Search Order"); + + [so release]; +} + +static void +testIvarAccess(void) +{ + SearchOrderNoIvarAccess *so = [SearchOrderNoIvarAccess new]; + + START_SET("Search Order Ivar Access"); + + PASS_EXCEPTION([so valueForKey:@"boolVal"], NSUndefinedKeyException, + "Does not return protected ivar"); + + END_SET("Search Order Ivar Access"); + + [so release]; +} + +int +main(int argc, char *argv[]) +{ + testSearchOrder(); + testIvarAccess(); + return 0; +} diff --git a/Tests/base/KVC/type_encoding.m b/Tests/base/KVC/type_encoding.m new file mode 100644 index 0000000000..2911705d1e --- /dev/null +++ b/Tests/base/KVC/type_encoding.m @@ -0,0 +1,16 @@ +#import + +#import "Testing.h" +#import "../../../Source/typeEncodingHelper.h" + +int main(int argc, char *argv[]) { + START_SET("Known Struct Type Encodings") + + PASS(strncmp(@encode(NSPoint), CGPOINT_ENCODING_PREFIX, strlen(CGPOINT_ENCODING_PREFIX)) == 0, "CGPoint encoding"); + PASS(strncmp(@encode(NSSize), CGSIZE_ENCODING_PREFIX, strlen(CGSIZE_ENCODING_PREFIX)) == 0, "CGSize encoding"); + PASS(strncmp(@encode(NSRect), CGRECT_ENCODING_PREFIX, strlen(CGRECT_ENCODING_PREFIX)) == 0, "CGRect encoding"); + PASS(strncmp(@encode(NSRange), NSRANGE_ENCODING_PREFIX, strlen(NSRANGE_ENCODING_PREFIX)) == 0, "NSRange encoding"); + + END_SET("Known Struct Type Encodings") + return 0; +} diff --git a/Tests/base/KVC/types.m b/Tests/base/KVC/types.m new file mode 100644 index 0000000000..3df85e62d1 --- /dev/null +++ b/Tests/base/KVC/types.m @@ -0,0 +1,354 @@ +#include "GNUstepBase/GSObjCRuntime.h" +#import +#import +#import +#import + +#include "Testing.h" + +/* + * Testing key-value coding on accessors and instance variable + * with all supported types. + * + * Please note that 'Class', `SEL`, unions, and pointer types are + * not coding-compliant on macOS. + */ + +typedef struct +{ + int x; + float y; +} MyStruct; + +@interface ReturnTypes : NSObject +{ + signed char _iChar; + int _iInt; + short _iShort; + long _iLong; + long long _iLongLong; + unsigned char _iUnsignedChar; + unsigned int _iUnsignedInt; + unsigned short _iUnsignedShort; + unsigned long _iUnsignedLong; + unsigned long long _iUnsignedLongLong; + float _iFloat; + double _iDouble; + bool _iBool; + // Not coding-compliant on macOS + // const char *_iCharPtr; + // int *_iIntPtr; + // Class _iCls; + // void *_iUnknownType; // Type encoding: ? + // MyUnion _iMyUnion; + id _iId; + + NSPoint _iNSPoint; + NSRange _iNSRange; + NSRect _iNSRect; + NSSize _iNSSize; + + MyStruct _iMyStruct; +} + +- (instancetype)init; + +- (signed char)mChar; // Type encoding: c +- (int)mInt; // Type encoding: i +- (short)mShort; // Type encoding: s +- (long)mLong; // Type encoding: l +- (long long)mLongLong; // Type encoding: q +- (unsigned char)mUnsignedChar; // Type encoding: C +- (unsigned int)mUnsignedInt; // Type encoding: I +- (unsigned short)mUnsignedShort; // Type encoding: S +- (unsigned long)mUnsignedLong; // Type encoding: L +- (unsigned long long)mUnsignedLongLong; // Type encoding: Q +- (float)mFloat; // Type encoding: f +- (double)mDouble; // Type encoding: d +- (bool)mBool; // Type encoding: B +- (id)mId; // Type encoding: @ + +- (NSPoint)mNSPoint; +- (NSRange)mNSRange; +- (NSRect)mNSRect; +- (NSSize)mNSSize; + +- (MyStruct)mMyStruct; +@end + +@implementation ReturnTypes + +- (instancetype)init +{ + self = [super init]; + if (self) + { + MyStruct my = {.x = 42, .y = 3.14f}; + _iChar = SCHAR_MIN; + _iShort = SHRT_MIN; + _iInt = INT_MIN; + _iLong = LONG_MIN; + _iUnsignedChar = 0; + _iUnsignedInt = 0; + _iUnsignedShort = 0; + _iUnsignedLong = 0; + _iUnsignedLongLong = 0; + _iFloat = 123.4f; + _iDouble = 123.45678; + _iBool = true; + _iId = @"id"; + _iNSPoint = NSMakePoint(1.0, 2.0); + _iNSRange = NSMakeRange(1, 2); + _iNSRect = NSMakeRect(1.0, 2.0, 3.0, 4.0); + _iNSSize = NSMakeSize(1.0, 2.0); + _iMyStruct = my; + } + return self; +} + +- (void)mVoid +{} + +- (signed char)mChar +{ + return SCHAR_MAX; +} + +- (short)mShort +{ + return SHRT_MAX; +} + +- (int)mInt +{ + return INT_MAX; +} + +- (long)mLong +{ + return LONG_MAX; +} + +- (long long)mLongLong +{ + return LLONG_MAX; +} + +- (unsigned char)mUnsignedChar +{ + return UCHAR_MAX; +} + +- (unsigned int)mUnsignedInt +{ + return UINT_MAX; +} + +- (unsigned short)mUnsignedShort +{ + return USHRT_MAX; +} + +- (unsigned long)mUnsignedLong +{ + return ULONG_MAX; +} + +- (unsigned long long)mUnsignedLongLong +{ + return ULLONG_MAX; +} + +- (float)mFloat +{ + return 123.45f; +} + +- (double)mDouble +{ + return 123.456789; +} + +- (bool)mBool +{ + return true; +} + +- (id)mId +{ + return @"id"; +} + +- (NSPoint)mNSPoint +{ + return NSMakePoint(1.0, 2.0); +} + +- (NSRange)mNSRange +{ + return NSMakeRange(1, 2); +} + +- (NSRect)mNSRect +{ + return NSMakeRect(1.0, 2.0, 3.0, 4.0); +} + +- (NSSize)mNSSize +{ + return NSMakeSize(1.0, 2.0); +} + +- (MyStruct)mMyStruct +{ + MyStruct s = {.x = 1, .y = 2.0}; + return s; +} + +@end + +static void +testAccessors(void) +{ + ReturnTypes *rt = [ReturnTypes new]; + + NSPoint p = NSMakePoint(1.0, 2.0); + NSRange r = NSMakeRange(1, 2); + NSRect re = NSMakeRect(1.0, 2.0, 3.0, 4.0); + NSSize s = NSMakeSize(1.0, 2.0); + MyStruct ms = {.x = 1, .y = 2.0}; + + START_SET("Accessors"); + + PASS_EQUAL([rt valueForKey:@"mChar"], [NSNumber numberWithChar:SCHAR_MAX], + "Accessor returns char"); + PASS_EQUAL([rt valueForKey:@"mInt"], [NSNumber numberWithInt:INT_MAX], + "Accessor returns int"); + PASS_EQUAL([rt valueForKey:@"mShort"], [NSNumber numberWithShort:SHRT_MAX], + "Accessor returns short"); + PASS_EQUAL([rt valueForKey:@"mLong"], [NSNumber numberWithLong:LONG_MAX], + "Accessor returns long"); + PASS_EQUAL([rt valueForKey:@"mLongLong"], + [NSNumber numberWithLongLong:LLONG_MAX], + "Accessor returns long long"); + PASS_EQUAL([rt valueForKey:@"mUnsignedChar"], + [NSNumber numberWithUnsignedChar:UCHAR_MAX], + "Accessor returns unsigned char"); + PASS_EQUAL([rt valueForKey:@"mUnsignedInt"], + [NSNumber numberWithUnsignedInt:UINT_MAX], + "Accessor returns unsigned int"); + PASS_EQUAL([rt valueForKey:@"mUnsignedShort"], + [NSNumber numberWithUnsignedShort:USHRT_MAX], + "Accessor returns unsigned short"); + PASS_EQUAL([rt valueForKey:@"mUnsignedLong"], + [NSNumber numberWithUnsignedLong:ULONG_MAX], + "Accessor returns unsigned long"); + PASS_EQUAL([rt valueForKey:@"mUnsignedLongLong"], + [NSNumber numberWithUnsignedLongLong:ULLONG_MAX], + "Accessor returns unsigned long long"); + PASS_EQUAL([rt valueForKey:@"mFloat"], [NSNumber numberWithFloat:123.45f], + "Accessor returns float"); + PASS_EQUAL([rt valueForKey:@"mDouble"], + [NSNumber numberWithDouble:123.456789], "Accessor returns double"); + PASS_EQUAL([rt valueForKey:@"mBool"], [NSNumber numberWithBool:true], + "Accessor returns bool"); + PASS_EQUAL([rt valueForKey:@"mId"], @"id", "Accessor returns id"); + PASS_EQUAL([rt valueForKey:@"mNSPoint"], [NSValue valueWithPoint:p], + "Accessor returns NSPoint"); + PASS_EQUAL([rt valueForKey:@"mNSRange"], [NSValue valueWithRange:r], + "Accessor returns NSRange"); + PASS_EQUAL([rt valueForKey:@"mNSRect"], [NSValue valueWithRect:re], + "Accessor returns NSRect"); + PASS_EQUAL([rt valueForKey:@"mNSSize"], [NSValue valueWithSize:s], + "Accessor returns NSSize"); + PASS_EQUAL([rt valueForKey:@"mMyStruct"], + [NSValue valueWithBytes:&ms objCType:@encode(MyStruct)], + "Accessor returns MyStruct"); + + END_SET("Accessors"); + + [rt release]; +} + +static void +testIvars(void) +{ + ReturnTypes *rt = [ReturnTypes new]; + + NSPoint p = NSMakePoint(1.0, 2.0); + NSRange r = NSMakeRange(1, 2); + NSRect re = NSMakeRect(1.0, 2.0, 3.0, 4.0); + NSSize s = NSMakeSize(1.0, 2.0); + MyStruct ms = {.x = 42, .y = 3.14f}; + + START_SET("Ivars"); + + PASS_EQUAL([rt valueForKey:@"iChar"], [NSNumber numberWithChar:SCHAR_MIN], + "Ivar returns char"); + PASS_EQUAL([rt valueForKey:@"iInt"], [NSNumber numberWithInt:INT_MIN], + "Ivar returns int"); + PASS_EQUAL([rt valueForKey:@"iShort"], [NSNumber numberWithShort:SHRT_MIN], + "Ivar returns short"); + PASS_EQUAL([rt valueForKey:@"iLong"], [NSNumber numberWithLong:LONG_MIN], + "Ivar returns long"); + PASS_EQUAL([rt valueForKey:@"iUnsignedChar"], + [NSNumber numberWithUnsignedChar:0], "Ivar returns unsigned char"); + PASS_EQUAL([rt valueForKey:@"iUnsignedInt"], + [NSNumber numberWithUnsignedInt:0], "Ivar returns unsigned int"); + PASS_EQUAL([rt valueForKey:@"iUnsignedShort"], + [NSNumber numberWithUnsignedShort:0], + "Ivar returns unsigned short"); + PASS_EQUAL([rt valueForKey:@"iUnsignedLong"], + [NSNumber numberWithUnsignedLong:0], "Ivar returns unsigned long"); + PASS_EQUAL([rt valueForKey:@"iUnsignedLongLong"], + [NSNumber numberWithUnsignedLongLong:0], + "Ivar returns unsigned long long"); + PASS_EQUAL([rt valueForKey:@"iFloat"], [NSNumber numberWithFloat:123.4f], + "Ivar returns float"); + PASS_EQUAL([rt valueForKey:@"iDouble"], [NSNumber numberWithDouble:123.45678], + "Ivar returns double"); + PASS_EQUAL([rt valueForKey:@"iBool"], [NSNumber numberWithBool:true], + "Ivar returns bool"); + PASS_EQUAL([rt valueForKey:@"iId"], @"id", "Ivar returns id"); + PASS_EQUAL([rt valueForKey:@"iNSPoint"], [NSValue valueWithPoint:p], + "Ivar returns NSPoint"); + PASS_EQUAL([rt valueForKey:@"iNSRange"], [NSValue valueWithRange:r], + "Ivar returns NSRange"); + PASS_EQUAL([rt valueForKey:@"iNSRect"], [NSValue valueWithRect:re], + "Ivar returns NSRect"); + PASS_EQUAL([rt valueForKey:@"iNSSize"], [NSValue valueWithSize:s], + "Ivar returns NSSize"); + + /* Welcome to another session of: Why GCC ObjC is a buggy mess. + * + * You'd expect that the type encoding of an ivar would be the same as @encode. + * + * Ivar var = class_getInstanceVariable([ReturnTypes class], "_iMyStruct"); + * const char *type = ivar_getTypeEncoding(var); + * NSLog(@"Type encoding of iMyStruct: %s", type); + * + * So type should be equal to @encode(MyStruct) ({?=if}) + * + * On GCC this is not the case. The type encoding of the ivar is {?="x"i"y"f}. + * This leads to failure of the following test. + * + * So mark this as hopeful until we stop supporting buggy compilers. + */ + testHopeful = YES; + PASS_EQUAL([rt valueForKey:@"iMyStruct"], + [NSValue valueWithBytes:&ms objCType:@encode(MyStruct)], + "Ivar returns MyStruct"); + testHopeful = NO; + END_SET("Ivars"); + + [rt release]; +} + +int +main(int argc, char *argv[]) +{ + testAccessors(); + testIvars(); + + return 0; +} diff --git a/Tests/base/coding/NSArray.1.32bit b/Tests/base/coding/NSArray.1.32bit index 1ab019d590975c869340530284c5c455099954a7..f0eb687b8403c41b9ac67e784ccb9220e90b914a 100644 GIT binary patch delta 49 zcmbQhIF(V{-7mDbB(*>xu_!qsvnxu_!qsvnxQS8R-7mDbB(*>xu_!qsvnxu_!qsvnxu_!qsvnf%wX{V*T^lYfKp?svhq@WJ$#@@yaLjH zlAZJEWJlk8Q@hwjzb!db%lg;r6p#GzJeDNIs7aohM2~f5GU`}234sqplo>@`>*nuY zzKZEgG)zl9$flKP(Yr1Ue?Mt6@$de^0N{`CtD$3)3esx3&0SG_<0uX=z1Rwwb2tWV=5P$##AOHaf zKmY;|fB*y_009U<00Izzz>Ns#+;n9m!NpK-KGRQU=aGbBkkUft5(@RR-~aVb%G@&? diff --git a/Tests/base/coding/NSCharacterSet.0.64bit b/Tests/base/coding/NSCharacterSet.0.64bit index 1f80d50bfb3b8a23ca1e703209931bfd16502706..98f46b9652ca6b8a6fcbc2721a48e7cec88a0a49 100644 GIT binary patch delta 37 hcmccR_=<7DVkrgzCI(Rk1`ve=6q>fPOkS&?3jl^;26q4e literal 8410 zcmeI&y9&ZE6b9han~H-@?sd>Pk>cRy1>E8ge1KS@t)SExMORYi}qUo7Mi4TW5l_M)nFp^Vh^H$0|+kp4jBRyU`_BA!r3$c?hfSMsG&3dt4oOKYT` zZ`|4xu_!qsvn)n=-7mDbB(*>xu_!qsvnxu_!qsvnxu_!qsvnkL8w diff --git a/Tests/base/coding/NSDateFormatter.0.32bit b/Tests/base/coding/NSDateFormatter.0.32bit index e14ef22c89a9556822255cae1c29c3d29f8e6dca..5fac12bfb1c2ea2b974339c9a5c48f012b0f6a37 100644 GIT binary patch delta 49 zcmdnaxQ9{P-7mDbB(*>xu_!qsvnxu_!qsvnxu_!qsvnxu_!qsvnfW4-7mDbB(*>xu_!qsvnxu_!qsvnNBCu;g=Pna;lfZ33Rfx|C2 exTGjEFP(vbfssKCNV2Me2vHEhR9ISCp$Gt)Toqpc diff --git a/Tests/base/coding/NSMutableData.0.32bit b/Tests/base/coding/NSMutableData.0.32bit index fe8b9f6b2eb11cd5a6245054842c1d663785a28a..d1b95222503d658dca9a3b855920c08a48b98227 100644 GIT binary patch delta 86 zcmey#c#ToP-7mDbB(*>xu_!qsvn)n=-7mDbB(*>xu_!qsvnxu_!qsvnxu_!qsvnxu_!qsvnxu_!qsvnxu_!qsvnxu_!qsvnxu_!qsvnkhC<6lnD+@y*Ln#neFem~5 DVRQ@) delta 44 ycmZ3;IEPW(-7mDbB(*>xu_!qsvnxu_!qsvnxu_!qsvn@&Q5M9Z!6US}Aoxx`~z**{Wp%rc>d;%6lP zkX=jhp|pcEUQ2IxwNLwWroLXaX7#eP)%V4<5jc*@cW;krfnydH@@R*MW5FbYq0$p; z%R2yIlEX;pPZvK-)#4uD4SxIu8Np=)nbKd%i>3LtFgFNBasa8))2421vqXT&7!sv} zo5_SSKAWpmQ_qa00CY`{4Y$srsleZFpq%#OWh>kEj1 z91)YLS$4cY#4@G3>DDglIUd8hyMYsl98=^6yad8gmp^c!q%Zu4?WmCPr24Htwcm2* z@F*vS+hK=^k<6&$(`MOFKOzw6HYHb1jftt%IagYhmuA)l=~Y=7G4?@8ACz!?hjL$v z%zM(_lk&M;_*}8K%{J*~EcSg+(FYa0@}ixV)z7(Y)^*4@kb95^l|PyiRSROm)5=XY zk19v-L*<_B`tV%tLw@F8)3mYmf9g#B;A8o>P7gnxu_!qsvnM^7<o6a!^afpU5b`3yxsG5rY!EE`$WfErm+(B delta 88 zcmcc5c$iV#-7mDbB(*>xu_!qsvn*W`v>rXIX*~p>>RKcnWB0y?cnF>owD-;2Qeit7A diff --git a/Tests/base/coding/NSValue.3.32bit b/Tests/base/coding/NSValue.3.32bit index 7975537d112dc0950dcd03cc520e4a8b4f9f3f8e..5ecfc32a7fecbed970d8a8170f9aea0a45cfba64 100644 GIT binary patch delta 63 zcmX@Yc!E*f-7mDbB(*>xu_!qsvnxu_!qsvn%7VPpr4#VFhtn KnF>owD-;0;BoAi* diff --git a/Tests/base/coding/NSValue.3.64bit b/Tests/base/coding/NSValue.3.64bit index 525144f85bf3c536fcb80a9b147b0656f7d2d01a..5ecfc32a7fecbed970d8a8170f9aea0a45cfba64 100644 GIT binary patch delta 20 bcmX@fc!F_4DHo3@0|NsOW3{vU#0F0QII9IP delta 21 ccmX@Xc#?5KDL1bu0|NsuV|Bb=@Wgsg06w|}r~m)}