Skip to content

Commit f5f9200

Browse files
authored
fix: serialize extended-class allocate/register to prevent duplicate class names (#421)
1 parent e5e84e0 commit f5f9200

4 files changed

Lines changed: 131 additions & 22 deletions

File tree

NativeScript/runtime/ClassBuilder.cpp

Lines changed: 56 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,54 @@
11
#include "ClassBuilder.h"
22

3+
#include <mutex>
4+
5+
#include "UnfairLock.h"
6+
37
namespace tns {
48

9+
namespace {
10+
// objc_allocateClassPair only rejects names that are already *registered*, so
11+
// two threads extending the same class name concurrently can both allocate it
12+
// and register duplicate same-named classes (objc keeps both and name lookups
13+
// become ambiguous). Serialize the whole allocate -> register window.
14+
//
15+
// This lock is a leaf — the section never acquires a v8::Locker — so it cannot
16+
// form a cycle with the isolate locks.
17+
UnfairMutex extendedClassRegistrationMutex;
18+
19+
// Registered objc class names are never reclaimed and the ladder below
20+
// allocates suffixes densely under the lock, so a name's taken suffixes form a
21+
// contiguous prefix. Gallop + binary-search with objc_getClass probes to find
22+
// its end, so heavy same-name reuse (worker tests, HMR re-extends of one
23+
// class) stays O(log N) per extend with no side state to grow.
24+
int FirstFreeSuffix(const std::string& initialName) {
25+
auto taken = [&initialName](int i) {
26+
return objc_getClass((initialName + std::to_string(i)).c_str()) != nil;
27+
};
28+
int hi = 1;
29+
while (taken(hi)) {
30+
hi *= 2;
31+
}
32+
int lo = hi / 2;
33+
while (lo + 1 < hi) {
34+
int mid = lo + (hi - lo) / 2;
35+
if (taken(mid)) {
36+
lo = mid;
37+
} else {
38+
hi = mid;
39+
}
40+
}
41+
return hi;
42+
}
43+
44+
// Bounds only *consecutive* failures past the probed free position, i.e.
45+
// allocation failing for reasons other than an ordinary name collision (which
46+
// would otherwise loop forever holding the mutex).
47+
constexpr int kMaxConsecutiveAllocFailures = 100;
48+
} // namespace
49+
550
// Moved this method in a separate .cpp file because ARC destroys the class
651
// created with objc_allocateClassPair when the control leaves this method scope
7-
// TODO: revist this. Maybe a lock is needed regardless
852
Class ClassBuilder::GetExtendedClass(std::string baseClassName,
953
std::string staticClassName,
1054
std::string suffix) {
@@ -14,19 +58,23 @@ Class ClassBuilder::GetExtendedClass(std::string baseClassName,
1458
? staticClassName
1559
: baseClassName + suffix + "_" +
1660
std::to_string(++ClassBuilder::classNameCounter_);
17-
// here we could either call objc_getClass with the name to see if the class
18-
// already exists or we can just try allocating it, which will return nil if
19-
// the class already exists so we try allocating it every time to avoid race
20-
// conditions in case this method is being executed by multiple threads
61+
// Allocation failure is the collision signal (objc_getClass beforehand
62+
// would race), but that only detects *registered* names — hence the lock
63+
// spanning allocate -> register.
64+
std::lock_guard<UnfairMutex> lock(extendedClassRegistrationMutex);
2165
Class clazz = objc_allocateClassPair(baseClass, name.c_str(), 0);
2266

2367
if (clazz == nil) {
24-
int i = 1;
2568
std::string initialName = name;
26-
while (clazz == nil) {
27-
name = initialName + std::to_string(i++);
69+
int next = FirstFreeSuffix(initialName);
70+
for (int attempts = 0;
71+
clazz == nil && attempts < kMaxConsecutiveAllocFailures; attempts++) {
72+
name = initialName + std::to_string(next++);
2873
clazz = objc_allocateClassPair(baseClass, name.c_str(), 0);
2974
}
75+
if (clazz == nil) {
76+
return nil;
77+
}
3078
}
3179

3280
objc_registerClassPair(clazz);

NativeScript/runtime/ClassBuilder.mm

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363

6464
Class extendedClass = ClassBuilder::GetExtendedClass(baseClassName, staticClassName,
6565
std::to_string(isolateId) + "_");
66+
tns::Assert(extendedClass != nil, isolate);
6667
class_addProtocol(extendedClass, @protocol(TNSDerivedClass));
6768
class_addProtocol(object_getClass(extendedClass), @protocol(TNSDerivedClass));
6869

@@ -222,6 +223,7 @@
222223
auto isolateId = cache->getIsolateId();
223224
__block Class extendedClass = ClassBuilder::GetExtendedClass(
224225
baseClassName, extendedClassName, std::to_string(isolateId) + "_");
226+
tns::Assert(extendedClass != nil, isolate);
225227
class_addProtocol(extendedClass, @protocol(TNSDerivedClass));
226228
class_addProtocol(object_getClass(extendedClass), @protocol(TNSDerivedClass));
227229

NativeScript/runtime/SpinLock.h

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
11
#ifndef SpinLock_h
22
#define SpinLock_h
33

4-
54
/**
65
WARNING:
76
Do NOT use this.
8-
More ofthen than not a normal mutex is better and this spinlock is really unfair in multi threading
9-
This is only supposed to be used in places where the function is very fast and the expected concurrency is very low
10-
If any of those things are false, this WILL be slower and worse than a mutex or a read write mutex
11-
12-
The only place this is currently used is for caching selectors, which take ns to run and are not locked to a specific isolate.
7+
More ofthen than not a normal mutex is better and this spinlock is really
8+
unfair in multi threading This is only supposed to be used in places where the
9+
function is very fast and the expected concurrency is very low If any of those
10+
things are false, this WILL be slower and worse than a mutex or a read write
11+
mutex
12+
13+
The only place this is currently used is for caching selectors, which take ns
14+
to run and are not locked to a specific isolate.
15+
16+
For sections that can block or see bursty contention, use UnfairLock.h instead.
1317
*/
1418

1519
struct SpinMutex {
@@ -43,14 +47,9 @@ struct SpinMutex {
4347
};
4448

4549
struct SpinLock {
46-
SpinMutex& _mutex;
47-
SpinLock(SpinMutex& m) : _mutex(m) {
48-
_mutex.lock();
49-
}
50-
~SpinLock() {
51-
_mutex.unlock();
52-
}
50+
SpinMutex& _mutex;
51+
SpinLock(SpinMutex& m) : _mutex(m) { _mutex.lock(); }
52+
~SpinLock() { _mutex.unlock(); }
5353
};
5454

55-
5655
#endif /* SpinLock_h */

NativeScript/runtime/UnfairLock.h

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
#ifndef UnfairLock_h
2+
#define UnfairLock_h
3+
4+
#include <os/lock.h>
5+
6+
/**
7+
BasicLockable wrapper over os_unfair_lock — the fastest blocking lock on
8+
Darwin. Drive it with std::lock_guard<UnfairMutex>.
9+
10+
Prefer this over SpinLock for any critical section that can block (syscalls,
11+
objc runtime calls, allocation) or that can see bursty contention: waiters
12+
sleep in the kernel and donate their priority to the holder, while SpinLock
13+
busy-waits in userspace, invisible to the scheduler. Benchmarked on an
14+
M-series host: under an 8-thread burst on a short section os_unfair_lock is
15+
~30x faster than SpinLock at ~33x less CPU, and against a holder that sleeps
16+
mid-section waiters cost ~100x less CPU. SpinLock stays marginally faster
17+
(~0.2 ns/op) only for uncontended nanosecond-scale sections — its documented
18+
niche (selector caching).
19+
20+
NATIVESCRIPT_UNFAIR_LOCK_ADAPTIVE_SPIN opts lock() into
21+
os_unfair_lock_lock_with_options() with kernel-informed adaptive spinning —
22+
the approach Firefox adopted for its nanosecond-hot allocator locks:
23+
https://hacks.mozilla.org/2022/10/improving-firefox-responsiveness-on-macos/
24+
25+
Experiments only, never ship it:
26+
- PRIVATE API (os/lock_private.h); App Review flags the symbol.
27+
- It only wins for nanosecond-scale sections at low contention with the
28+
holder on-core (measured ~2x over plain os_unfair_lock at 2 threads).
29+
Under bursty contention it degenerates to spinlock behavior (~30x slower,
30+
~30x more CPU at 8 threads), it is the worst option under QoS inversion,
31+
and it changes nothing when the holder sleeps.
32+
*/
33+
34+
#ifdef NATIVESCRIPT_UNFAIR_LOCK_ADAPTIVE_SPIN
35+
extern "C" void os_unfair_lock_lock_with_options(os_unfair_lock_t lock,
36+
uint32_t options);
37+
// Values from os/lock_private.h (ADAPTIVE_SPIN requires iOS 13+).
38+
#define NATIVESCRIPT_OS_UNFAIR_LOCK_DATA_SYNCHRONIZATION 0x00010000u
39+
#define NATIVESCRIPT_OS_UNFAIR_LOCK_ADAPTIVE_SPIN 0x00040000u
40+
#endif
41+
42+
struct UnfairMutex {
43+
os_unfair_lock lock_ = OS_UNFAIR_LOCK_INIT;
44+
45+
inline void lock() noexcept {
46+
#ifdef NATIVESCRIPT_UNFAIR_LOCK_ADAPTIVE_SPIN
47+
os_unfair_lock_lock_with_options(
48+
&lock_, NATIVESCRIPT_OS_UNFAIR_LOCK_DATA_SYNCHRONIZATION |
49+
NATIVESCRIPT_OS_UNFAIR_LOCK_ADAPTIVE_SPIN);
50+
#else
51+
os_unfair_lock_lock(&lock_);
52+
#endif
53+
}
54+
55+
inline bool try_lock() noexcept { return os_unfair_lock_trylock(&lock_); }
56+
57+
inline void unlock() noexcept { os_unfair_lock_unlock(&lock_); }
58+
};
59+
60+
#endif /* UnfairLock_h */

0 commit comments

Comments
 (0)