Skip to content

Commit

Permalink
NSKeyValueCoding: Safe-Caching for -[NSObject valueForKey:] (#445)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
hmelder and rfm authored Oct 29, 2024
1 parent 4f0a8d6 commit 6eef1c3
Show file tree
Hide file tree
Showing 37 changed files with 1,549 additions and 49 deletions.
126 changes: 126 additions & 0 deletions Headers/CoreFoundation/CFCGTypes.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/** CFCGTypes.h - CoreFoundation header file for CG types
Copyright (C) 2024 Free Software Foundation, Inc.
Written by: Hugo Melder <[email protected]>
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 <float.h>
#include <stdint.h>

#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
25 changes: 6 additions & 19 deletions Headers/Foundation/NSGeometry.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,12 @@
#ifndef __NSGeometry_h_GNUSTEP_BASE_INCLUDE
#define __NSGeometry_h_GNUSTEP_BASE_INCLUDE
#import <GNUstepBase/GSVersionMacros.h>
#import <CoreFoundation/CFCGTypes.h>

#ifdef __OBJC__
#import <objc/objc.h>

#import <Foundation/NSString.h>
#endif

#if defined(__cplusplus)
extern "C" {
Expand Down Expand Up @@ -56,12 +58,7 @@ extern "C" {
CGFloat y;
}</example>
<p>Represents a 2-d cartesian position.</p> */
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. */
Expand All @@ -76,12 +73,7 @@ typedef NSPoint *NSPointPointer;
CGFloat height;
}</example>
<p>Floating point rectangle size.</p> */
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. */
Expand All @@ -97,12 +89,7 @@ typedef NSSize *NSSizePointer;
}</example>
<p>Rectangle.</p> */
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. */
Expand Down
13 changes: 0 additions & 13 deletions Headers/Foundation/NSObjCRuntime.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 7 additions & 4 deletions Source/Additions/GSObjCRuntime.m
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@

#import "../GSPrivate.h"
#import "../GSPThread.h"
#import "../typeEncodingHelper.h"

#include <objc/Protocol.h>

Expand Down Expand Up @@ -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;

Expand All @@ -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;

Expand All @@ -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;

Expand All @@ -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;

Expand Down Expand Up @@ -1410,6 +1412,7 @@ unsigned long long (*imp)(id, SEL) =
}
}
break;
}

default:
#ifdef __GNUSTEP_RUNTIME__
Expand Down
13 changes: 12 additions & 1 deletion Source/GNUmakefile
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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 \
Expand Down Expand Up @@ -586,7 +596,8 @@ NSZone.h
HEADERS_INSTALL = \
$(OBJECTIVEC2_HEADERS) \
$(GNUSTEPBASE_HEADERS) \
$(FOUNDATION_HEADERS)
$(FOUNDATION_HEADERS) \
$(COREFOUNDATION_HEADERS)

GENERATED_HFILES = \
dynamic-load.h \
Expand Down
5 changes: 5 additions & 0 deletions Source/Makefile.postamble
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand Down
55 changes: 55 additions & 0 deletions Source/NSKeyValueCoding+Caching.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/** Key-Value Coding Safe Caching Support
Copyright (C) 2024 Free Software Foundation, Inc.
Written by: Hugo Melder <[email protected]>
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);
Loading

0 comments on commit 6eef1c3

Please sign in to comment.