-
Notifications
You must be signed in to change notification settings - Fork 64
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
[ffigen] Protocol polish #2060
Conversation
PR HealthBreaking changes ✔️
API leaks
|
Package | Leaked API symbols |
---|---|
objective_c | _Version |
This check can be disabled by tagging the PR with skip-leaking-check
.
License Headers ✔️
// Copyright (c) 2025, 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.
Files |
---|
no missing headers |
All source files should start with a license header.
Unrelated files missing license headers
Files |
---|
pkgs/jni/lib/src/third_party/generated_bindings.dart |
pkgs/objective_c/lib/src/ns_input_stream.dart |
pkgs/objective_c/src/protocol.m
Outdated
|
||
#if !__has_feature(objc_arc) | ||
#error "This file must be compiled with ARC enabled" | ||
#endif | ||
|
||
@implementation DOBJCDartProtocolBuilder { | ||
NSMutableDictionary *methods; | ||
@interface DOBJCDartProtocolClass : NSObject |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 containDOBJC
, to minimize the chances that they collide with a method that the user is trying to implement.
Makes sense.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM with comments
pkgs/objective_c/src/protocol.m
Outdated
- (void)implementMethod:(SEL)sel withBlock:(void*)block { | ||
- (void)implementMethod:(SEL)sel withBlock:(void*)block | ||
withTrampoline:(void*)trampoline withSignature:(char*)signature { | ||
class_addMethod(clazz, sel, trampoline, signature); | ||
[self implement:sel withBlock:(__bridge id)block]; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
@@ -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}) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
[self implement:sel withBlock:(__bridge id)block]; | ||
} | ||
|
||
- (NSDictionary*)copyMethods NS_RETURNS_RETAINED { | ||
return [methods copy]; | ||
- (void)registerClass { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Question: should we be also calling class_addProtocol
to make code which checks protocol conformance work?
I think right now if somebody asks our protocols dynamically ([dartProtocol conformsToProtocol:@protocol(Whatever)]
) they will get NO
even though dartProtocol
has all necessary methods.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
pkgs/objective_c/src/protocol.m
Outdated
|
||
#if !__has_feature(objc_arc) | ||
#error "This file must be compiled with ARC enabled" | ||
#endif | ||
|
||
@implementation DOBJCDartProtocolBuilder { | ||
NSMutableDictionary *methods; | ||
@interface DOBJCDartProtocolClass : NSObject |
There was a problem hiding this comment.
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 containDOBJC
, to minimize the chances that they collide with a method that the user is trying to implement.
Makes sense.
Clean up the class object when the implementation object and the builder have been destroyed, by wrapping it in a
DOBJCDartProtocolClass
object that callsobjc_disposeClassPair
in itsdispose
method, and having the builder and instances hold a reference to this object.Also, since we're not allowing adding more methods after building the protocol, we don't have to copy the method table during instantiation anymore.
Finally, re-add a bunch of other GC tests that I accidentally deleted.
Fixes #2042