| // Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file |
| // for details. All rights reserved. Use of this source code is governed by a |
| // BSD-style license that can be found in the LICENSE file. |
| |
| // GC injection helpers for regression-testing issue #3209. |
| // |
| // Swizzles DOBJCDartProtocolBuilder's implementMethod:withBlock:... to inject |
| // a Dart GC at the FFI safepoint between Dart extracting the raw block pointer |
| // and Objective-C retaining it — the exact window where premature GC can |
| // release the block before the handover completes. |
| // |
| // Run AOT to reproduce under production-like conditions — GC liveness issues |
| // like this are unreliable in JIT (from pkgs/objective_c/). |
| // Native assets must be enabled; stable from Dart 3.10.0: |
| // dart compile exe test/gc_safepoint_test.dart -o /tmp/gc_test |
| // DYLD_INSERT_LIBRARIES=.dart_tool/lib/objective_c.dylib /tmp/gc_test |
| |
| #import <Foundation/Foundation.h> |
| #import <objc/runtime.h> |
| #include <dlfcn.h> |
| #include <stdbool.h> |
| #include <stdint.h> |
| |
| typedef struct { |
| void* isa; |
| int flags; |
| } BlockRefCountExtractor; |
| |
| __attribute__((visibility("default"))) |
| uint64_t getBlockRetainCount(BlockRefCountExtractor* block) { |
| return (block->flags & 0xFFFF) >> 1; |
| } |
| |
| typedef void* (*DartGCNow_t)(const char*, void*); |
| static DartGCNow_t g_dart_gc_now = NULL; |
| |
| static IMP g_original_imp = NULL; |
| static bool g_swizzle_active = false; |
| // Written and read from the same Dart thread (native-mode FFI call), so a |
| // plain bool is safe. |
| static bool g_block_freed_before_retain = false; |
| |
| // Replacement for -[DOBJCDartProtocolBuilder implementMethod:withBlock:...]. |
| // Forces GC before calling the original, then checks the retain count. |
| static void gc_inject_imp( |
| id self, SEL _cmd, SEL sel, void* block, |
| void* trampoline, const char* signature) { |
| if (g_swizzle_active && g_dart_gc_now != NULL) { |
| g_dart_gc_now("gc-now", NULL); |
| // Use the same util as the Dart side (util.c:getBlockRetainCount). |
| int count = (int)getBlockRetainCount(block); |
| if (count == 0) { |
| g_block_freed_before_retain = true; |
| } |
| } |
| ((void (*)(id, SEL, SEL, void*, void*, const char*))g_original_imp)( |
| self, _cmd, sel, block, trampoline, signature); |
| } |
| |
| // Look up Dart_ExecuteInternalCommand via dlsym. Must be called once before |
| // installing the swizzle. |
| void initGCInject(void) { |
| g_dart_gc_now = |
| (DartGCNow_t)dlsym(RTLD_DEFAULT, "Dart_ExecuteInternalCommand"); |
| } |
| |
| bool gcNowAvailableFromNative(void) { |
| return g_dart_gc_now != NULL; |
| } |
| |
| // Triggers gc-now from inside a non-leaf FFI call (Dart thread is at a |
| // safepoint), used to verify GC can actually fire from native mode. |
| void callGCNowFromNative(void) { |
| if (g_dart_gc_now != NULL) { |
| g_dart_gc_now("gc-now", NULL); |
| } |
| } |
| |
| void installGCInjectSwizzle(void) { |
| g_block_freed_before_retain = false; |
| Class cls = NSClassFromString(@"DOBJCDartProtocolBuilder"); |
| if (cls == nil) return; |
| SEL sel = |
| @selector(implementMethod:withBlock:withTrampoline:withSignature:); |
| Method m = class_getInstanceMethod(cls, sel); |
| if (m == NULL) return; |
| g_original_imp = method_setImplementation(m, (IMP)gc_inject_imp); |
| } |
| |
| void removeGCInjectSwizzle(void) { |
| if (g_original_imp == NULL) return; |
| Class cls = NSClassFromString(@"DOBJCDartProtocolBuilder"); |
| SEL sel = |
| @selector(implementMethod:withBlock:withTrampoline:withSignature:); |
| Method m = class_getInstanceMethod(cls, sel); |
| if (m != NULL) { |
| method_setImplementation(m, g_original_imp); |
| } |
| g_original_imp = NULL; |
| } |
| |
| void setGCInjectActive(bool active) { |
| g_swizzle_active = active; |
| } |
| |
| // Sticky flag: once true it stays true even after setGCInjectActive(false). |
| bool wasBlockFreedBeforeRetain(void) { |
| return g_block_freed_before_retain; |
| } |