Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ffigen] Protocol polish #2060

Merged
merged 15 commits into from
Mar 6, 2025
4 changes: 4 additions & 0 deletions pkgs/ffigen/test/native_objc_test/protocol_config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ output:
bindings: 'protocol_bindings.dart'
objc-bindings: 'protocol_bindings.m'
exclude-all-by-default: true
functions:
include:
- getClass
- getClassName
objc-interfaces:
include:
- ProtocolConsumer
Expand Down
105 changes: 104 additions & 1 deletion pkgs/ffigen/test/native_objc_test/protocol_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,15 @@ typedef VoidMethodBlock = ObjCBlock_ffiVoid_ffiVoid_Int32;
typedef OtherMethodBlock = ObjCBlock_Int32_ffiVoid_Int32_Int32_Int32_Int32;

void main() {
late ProtocolTestObjCLibrary lib;

group('protocol', () {
setUpAll(() {
// TODO(https://github.com/dart-lang/native/issues/1068): Remove this.
DynamicLibrary.open('../objective_c/test/objective_c.dylib');
final dylib = File('test/native_objc_test/objc_test.dylib');
verifySetupFile(dylib);
DynamicLibrary.open(dylib.absolute.path);
lib = ProtocolTestObjCLibrary(DynamicLibrary.open(dylib.absolute.path));
generateBindingsForCoverage('protocol');
});

Expand Down Expand Up @@ -413,6 +415,69 @@ void main() {
expect(UnusedProtocol.conformsTo(inst), isFalse);
});

test('Threading stress test', () async {
final consumer = ProtocolConsumer();
final completer = Completer<void>();
int count = 0;

final protocolBuilder = ObjCProtocolBuilder();
MyProtocol.voidMethod_.implementAsListener(protocolBuilder, (int x) {
expect(x, 123);
++count;
if (count == 1000) completer.complete();
});

final protocol = protocolBuilder.build();
final MyProtocol asMyProtocol = MyProtocol.castFrom(protocol);

for (int i = 0; i < 1000; ++i) {
consumer.callMethodOnRandomThread_(asMyProtocol);
}
await completer.future;
expect(count, 1000);
});

(NSObject, Pointer<ObjCBlockImpl>) blockRefCountTestInner() {
final protocolBuilder = ObjCProtocolBuilder();

final block = InstanceMethodBlock.fromFunction(
(Pointer<Void> p, NSString s, double x) => 'Hello'.toNSString());
MyProtocol.instanceMethod_withDouble_
.implementWithBlock(protocolBuilder, block);
final protocol = protocolBuilder.build();

final blockPtr = block.ref.pointer;

// There are 2 references to the block. One owned by the Dart wrapper
// object, and the other owned by the protocol.
doGC();
expect(blockRetainCount(blockPtr), 2);

return (protocol, blockPtr);
}

Pointer<ObjCBlockImpl> blockRefCountTest() {
final (protocol, blockPtr) = blockRefCountTestInner();

// The Dart side block pointer has gone out of scope, but the protocol
// still owns a reference to it.
doGC();
expect(blockRetainCount(blockPtr), 1);

expect(protocol, isNotNull); // Force protocol to stay in scope.

return blockPtr;
}

test('Block ref counting', () {
final blockPtr = blockRefCountTest();

// The protocol object has gone out of scope, so it should be cleaned up.
// So should the block.
doGC();
expect(blockRetainCount(blockPtr), 0);
}, skip: !canDoGC);

test('keepIsolateAlive', () async {
final isolateSendPort = Completer<SendPort>();
final protosCreated = Completer<void>();
Expand Down Expand Up @@ -475,5 +540,43 @@ void main() {

receivePort.close();
}, skip: !canDoGC);

test('class disposal, builder first', () {
ObjCProtocolBuilder? protocolBuilder =
ObjCProtocolBuilder(debugName: 'Foo');

NSObject? protocol = protocolBuilder.build();
final clazz = lib.getClass(protocol);
expect(lib.getClassName(clazz).cast<Utf8>().toDartString(),
startsWith('Foo'));
expect(isValidClass(clazz), isTrue);

protocolBuilder = null;
doGC();
expect(isValidClass(clazz), isTrue);

protocol = null;
doGC();
expect(isValidClass(clazz), isFalse);
}, skip: !canDoGC);

test('class disposal, instance first', () {
ObjCProtocolBuilder? protocolBuilder =
ObjCProtocolBuilder(debugName: 'Foo');

NSObject? protocol = protocolBuilder.build();
final clazz = lib.getClass(protocol);
expect(lib.getClassName(clazz).cast<Utf8>().toDartString(),
startsWith('Foo'));
expect(isValidClass(clazz), isTrue);

protocolBuilder = null;
doGC();
expect(isValidClass(clazz), isTrue);

protocol = null;
doGC();
expect(isValidClass(clazz), isFalse);
}, skip: !canDoGC);
});
}
3 changes: 3 additions & 0 deletions pkgs/ffigen/test/native_objc_test/protocol_test.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
#import <Foundation/NSObject.h>
#import <Foundation/NSString.h>

const char* getClassName(void* cls);
void* getClass(id object);

typedef struct {
int32_t x;
int32_t y;
Expand Down
10 changes: 10 additions & 0 deletions pkgs/ffigen/test/native_objc_test/protocol_test.m
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,16 @@

#include "protocol_test.h"

const char* class_getName(Class cls);

const char* getClassName(void* cls) {
return class_getName((__bridge Class)cls);
}

void* getClass(id object) {
return (__bridge void*)[object class];
}

@implementation ProtocolConsumer : NSObject
- (NSString*)callInstanceMethod:(id<SuperProtocol>)protocol {
return [protocol instanceMethod:@"Hello from ObjC" withDouble:3.14];
Expand Down
3 changes: 3 additions & 0 deletions pkgs/ffigen/test/native_objc_test/util.dart
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,6 @@ int objectRetainCount(Pointer<ObjCObject> object) {
if (!internal_for_testing.isValidClass(clazz)) return 0;
return _getObjectRetainCount(object.cast());
}

bool isValidClass(Pointer<Void> clazz) =>
internal_for_testing.isValidClass(clazz.cast(), bypassCache: true);
7 changes: 4 additions & 3 deletions pkgs/objective_c/lib/src/internal.dart
Original file line number Diff line number Diff line change
Expand Up @@ -295,8 +295,8 @@ bool _isValidObject(ObjectPtr ptr) {

final _allClasses = <ObjectPtr>{};

bool _isValidClass(ObjectPtr clazz) {
if (_allClasses.contains(clazz)) return true;
bool _isValidClass(ObjectPtr clazz, {bool bypassCache = false}) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: a more accurate name for bypassCache is probably forceClassListReload or something like this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

if (!bypassCache && _allClasses.contains(clazz)) return true;

// If the class is missing from the list, it either means we haven't created
// the set yet, or more classes have been loaded since we created the set, or
Expand Down Expand Up @@ -446,5 +446,6 @@ BlockPtr wrapBlockingBlock(
bool blockHasRegisteredClosure(BlockPtr block) =>
_blockClosureRegistry.containsKey(block.ref.target.address);
bool isValidBlock(BlockPtr block) => c.isValidBlock(block);
bool isValidClass(ObjectPtr clazz) => _isValidClass(clazz);
bool isValidClass(ObjectPtr clazz, {bool bypassCache = false}) =>
_isValidClass(clazz, bypassCache: bypassCache);
bool isValidObject(ObjectPtr object) => _isValidObject(object);
17 changes: 17 additions & 0 deletions pkgs/objective_c/lib/src/objective_c_bindings_generated.dart
Original file line number Diff line number Diff line change
Expand Up @@ -620,6 +620,14 @@ class DartProtocolBuilder extends NSObject {
retain: false, release: true);
}

/// initWithClass:
DartProtocolBuilder initWithClass_(ffi.Pointer<ffi.Void> cls) {
final _ret = _objc_msgSend_1mbt9g9(
this.ref.retainAndReturnPointer(), _sel_initWithClass_, cls);
return DartProtocolBuilder.castFromPointer(_ret,
retain: false, release: true);
}

/// retain
DartProtocolBuilder retain() {
final _ret = _objc_msgSend_151sglz(this.ref.pointer, _sel_retain);
Expand Down Expand Up @@ -16024,6 +16032,14 @@ final _objc_msgSend_1lv8yz3 = objc.msgSendPointer
.asFunction<
bool Function(ffi.Pointer<objc.ObjCObject>,
ffi.Pointer<objc.ObjCSelector>, ffi.Pointer<ffi.Char>, int, int)>();
final _objc_msgSend_1mbt9g9 = objc.msgSendPointer
.cast<
ffi.NativeFunction<
ffi.Pointer<objc.ObjCObject> Function(ffi.Pointer<objc.ObjCObject>,
ffi.Pointer<objc.ObjCSelector>, ffi.Pointer<ffi.Void>)>>()
.asFunction<
ffi.Pointer<objc.ObjCObject> Function(ffi.Pointer<objc.ObjCObject>,
ffi.Pointer<objc.ObjCSelector>, ffi.Pointer<ffi.Void>)>();
final _objc_msgSend_1n40f6p = objc.msgSendPointer
.cast<
ffi.NativeFunction<
Expand Down Expand Up @@ -17683,6 +17699,7 @@ late final _sel_initWithCharactersNoCopy_length_freeWhenDone_ =
objc.registerName("initWithCharactersNoCopy:length:freeWhenDone:");
late final _sel_initWithCharacters_length_ =
objc.registerName("initWithCharacters:length:");
late final _sel_initWithClass_ = objc.registerName("initWithClass:");
late final _sel_initWithCoder_ = objc.registerName("initWithCoder:");
late final _sel_initWithContentsOfFile_ =
objc.registerName("initWithContentsOfFile:");
Expand Down
8 changes: 6 additions & 2 deletions pkgs/objective_c/lib/src/protocol_builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,16 @@ import 'selector.dart';

/// Helper class for building Objective C objects that implement protocols.
class ObjCProtocolBuilder {
final _builder = objc.DartProtocolBuilder();
final objc.DartProtocolBuilder _builder;
final Pointer<c.ObjCObject> _class;
var _built = false;

ObjCProtocolBuilder._(this._class)
: _builder =
objc.DartProtocolBuilder.alloc().initWithClass_(_class.cast());

ObjCProtocolBuilder({String? debugName})
: _class = _createProtocolClass(debugName);
: this._(_createProtocolClass(debugName));

/// Add a method implementation to the protocol.
///
Expand Down
3 changes: 1 addition & 2 deletions pkgs/objective_c/src/protocol.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@
#include "include/dart_api_dl.h"

@interface DOBJCDartProtocolBuilder : NSObject
+ (instancetype)new;
- (instancetype)init;
- (instancetype)initWithClass: (void*)cls;
- (void)implementMethod:(SEL) sel withBlock:(void*)block;
@end

Expand Down
43 changes: 35 additions & 8 deletions pkgs/objective_c/src/protocol.m
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,42 @@
#import <Foundation/NSDictionary.h>
#import <Foundation/NSInvocation.h>
#import <Foundation/NSValue.h>
#import <objc/runtime.h>

#if !__has_feature(objc_arc)
#error "This file must be compiled with ARC enabled"
#endif

@implementation DOBJCDartProtocolBuilder {
NSMutableDictionary *methods;
@interface DOBJCDartProtocolClass : NSObject
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you have a specific reason to separate this from the builder itself?

Consider a simpler setup where DOBJCDartProtocol just holds DOBJCDartProtocolBuilder and also where we create class in Objective-C and not on the Dart side. Below is a rough sketch.

I also tried to follow Google Objective-C Style Guide which we otherwise seemed to have ignored. It is good to have more comments and somewhat cleaner code.

@class DOBJCDartProtocol;

/**
 * Class responsible for creating Objective-C classes in runtime.
 * See also `...` in Dart.
 */
@interface DOBJCDartProtocolBuilder : NSObject

/**
 * Initialize the builder using the given @c debugName.
 * 
 * @param debugName prefix to use when creating an Objective-C class,
 *   "DOBJCDartProtocol" will be used if @c debugName is @c nil
 */
- (instancetype)initWithDebugName: (NSString*)debugName;

/**
 * Use @c block to implement selector @c sel.
 * 
 * This method can only be called before the first instance is created
 * via @c newInstanceWithDisposePort.
 * 
 * @param sel Selector which will be implemented.
 * @param block Block to redirect method to.
 */
- (void)implementMethod:(SEL) sel withBlock:(void*)block;

/**
 * Create an instance of the class built by this builder.
 * 
 * Finalizes and registers Objective-C class. No new methods can be implemented
 * after the first instance is created.
 * 
 * @param port Optional port to notify when newly created instance is 
 *   deallocated. Can be @c ILLEGAL_PORT.
 */ 
- (DOBJCDartProtocol*)newInstanceWithDisposePort:(Dart_Port)port;

- (void) dealloc;
@end

@interface DOBJCDartProtocol : NSObject
- (instancetype)initFromBuilder: (DOBJCDartProtocolBuilder*)builder withDisposePort:(Dart_Port)port;

- (id)getBlockImplementingSelector:(SEL)sel;

- (void)dealloc;
@end

@implementation DOBJCDartProtocolBuilder {
  BOOL _finalized;
  Class _cls;
  @public NSMutableDictionary *_methods;
}

static _Atomic int64_t gNextProtocolId = 0;

- (instancetype)initWithDebugName: (NSString *)debugName {
  // Note: no need for if (self) if you don't do `self = [super init]`.
  if (!debugName) {
    debugName = @"DOBJCDartProtocol";
  }
  int64_t id = atomic_fetch_add(&gNextProtocolId, 1);
  debugName = [NSString stringWithFormat:@"%@_%lld",  debugName, id];
  _cls = objc_allocateClassPair(objc_lookUpClass("DOBJCDartProtocol"), [debugName UTF8String], /*extra_bytes=*/0);
  _methods = [NSMutableDictionary new];
  _finalized = NO;
  return self;
}

- (DOBJCDartProtocol*)newInstanceWithDisposePort:(Dart_Port)port {
  if (!_finalized) {
    objc_registerClassPair(_cls);
    _finalized = YES;
  }

  DOBJCDartProtocol* obj = [_cls alloc];
  return [obj initFromBuilder:self withDisposePort:port];
}

- (void) dealloc {
  objc_disposeClassPair(_cls);
}
@end

@implementation DOBJCDartProtocol {
  DOBJCDartProtocolBuilder* _builder;
  Dart_Port _dispose_port;
}

- (instancetype)initFromBuilder: (DOBJCDartProtocolBuilder*)builder withDisposePort:(Dart_Port)port {
  _builder = builder;
  _dispose_port = port;
  return self;
}

- (void)dealloc {
  if (_dispose_port != ILLEGAL_PORT) {
    Dart_PostInteger_DL(_dispose_port, 0);
  }
}

- (id)getBlockImplementingSelector:(SEL)sel {
  return [_builder->_methods objectForKey:[NSValue valueWithPointer:sel]];
}
@end

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, more or less. In general, all else being equal, I prefer to orchestrate in Dart (because it's a better language ;) ). But yeah, creating the class and instantiating the implementation objects on the ObjC side simplifies things a little, and the DOBJCDartProtocolClass is unnecessary.

My formatting is always a little weird because I haven't managed to get clang format to work on these files (or rather, it makes nonsensical formatting decisions, like ignoring like length).

DOBJCDartProtocol's methods (other than dealloc) are given such verbose names, and contain DOBJC, to minimize the chances that they collide with a method that the user is trying to implement.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Choices look reasonable.

I do also prefer orchestrating in Dart - however I usually apply the rule of "keeping the logic concentrated in one place". So if I am already writing native code and native code will look more natural - then I will shift most of the logic into native code and make Dart code a thin wrapper. This makes the code easier to understand - otherwise the logic gets split and you need to jump back and forth to piece it together.

DOBJCDartProtocol's methods (other than dealloc) are given such verbose names, and contain DOBJC, to minimize the chances that they collide with a method that the user is trying to implement.

Makes sense.

- (instancetype)initWithClass: (Class)cls;
@end

@implementation DOBJCDartProtocolClass {
Class clazz;
}

- (instancetype)initWithClass: (Class)cls {
if (self) {
clazz = cls;
}
return self;
}

+ (instancetype)new {
return [[self alloc] init];
- (void)dealloc {
objc_disposeClassPair(clazz);
}

- (instancetype)init {
@end

@implementation DOBJCDartProtocolBuilder {
NSMutableDictionary *methods;
DOBJCDartProtocolClass* clazz;
}

- (instancetype)initWithClass: (void*)cls {
if (self) {
methods = [NSMutableDictionary new];
clazz = [[DOBJCDartProtocolClass alloc] initWithClass: (__bridge Class)cls];
}
return self;
}
Expand All @@ -37,13 +57,19 @@ - (void)implementMethod:(SEL)sel withBlock:(void*)block {
[self implement:sel withBlock:(__bridge id)block];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should [self implement] just be inlined? It is small and not used for anything else.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

}

- (NSDictionary*)copyMethods NS_RETURNS_RETAINED {
return [methods copy];
- (NSDictionary*)getMethods {
return methods;
}

- (DOBJCDartProtocolClass*)getClass {
return clazz;
}

@end

@implementation DOBJCDartProtocol {
NSDictionary *methods;
DOBJCDartProtocolClass* clazz;
Dart_Port dispose_port;
}

Expand All @@ -55,7 +81,8 @@ - (instancetype)initDOBJCDartProtocolFromDartProtocolBuilder:
(DOBJCDartProtocolBuilder*)builder
withDisposePort:(Dart_Port)port {
if (self) {
methods = [builder copyMethods];
methods = [builder getMethods];
clazz = [builder getClass];
dispose_port = port;
}
return self;
Expand Down
Loading