Skip to content

Commit ec98149

Browse files
authored
[ffigen] Protocol polish (#2060)
1 parent 0df33b3 commit ec98149

15 files changed

+385
-129
lines changed

pkgs/ffigen/lib/src/code_generator/objc_built_in_functions.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ class ObjCBuiltInFunctions {
4343
static const protocolMethod = ObjCImport('ObjCProtocolMethod');
4444
static const protocolListenableMethod =
4545
ObjCImport('ObjCProtocolListenableMethod');
46+
static const protocolClass = ObjCImport('Protocol');
4647
static const protocolBuilder = ObjCImport('ObjCProtocolBuilder');
4748
static const unimplementedOptionalMethodException =
4849
ObjCImport('UnimplementedOptionalMethodException');

pkgs/ffigen/lib/src/code_generator/objc_protocol.dart

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ class ObjCProtocol extends BindingType with ObjCMethods {
6262

6363
@override
6464
BindingString toBindingString(Writer w) {
65+
final protocolClass = ObjCBuiltInFunctions.protocolClass.gen(w);
6566
final protocolBase = ObjCBuiltInFunctions.protocolBase.gen(w);
6667
final protocolMethod = ObjCBuiltInFunctions.protocolMethod.gen(w);
6768
final protocolListenableMethod =
@@ -180,6 +181,10 @@ interface class $name extends $protocolBase $impls{
180181
buildArgs.add('bool \$keepIsolateAlive = true');
181182
final args = '{${buildArgs.join(', ')}}';
182183
final builders = '''
184+
/// Returns the [$protocolClass] object for this protocol.
185+
static $protocolClass get \$protocol =>
186+
$protocolClass.castFromPointer(${_protocolPointer.name}.cast());
187+
183188
/// Builds an object that implements the $originalName protocol. To implement
184189
/// multiple protocols, use [addToBuilder] or [$protocolBuilder] directly.
185190
///
@@ -188,6 +193,7 @@ interface class $name extends $protocolBase $impls{
188193
static $name implement($args) {
189194
final builder = $protocolBuilder(debugName: '$originalName');
190195
$buildImplementations
196+
builder.addProtocol(\$protocol);
191197
return $name.castFrom(builder.build(keepIsolateAlive: \$keepIsolateAlive));
192198
}
193199
@@ -197,6 +203,7 @@ interface class $name extends $protocolBase $impls{
197203
/// Note: You cannot call this method after you have called `builder.build`.
198204
static void addToBuilder($protocolBuilder builder, $args) {
199205
$buildImplementations
206+
builder.addProtocol(\$protocol);
200207
}
201208
''';
202209

@@ -212,6 +219,7 @@ interface class $name extends $protocolBase $impls{
212219
static $name implementAsListener($args) {
213220
final builder = $protocolBuilder(debugName: '$originalName');
214221
$buildListenerImplementations
222+
builder.addProtocol(\$protocol);
215223
return $name.castFrom(builder.build(keepIsolateAlive: \$keepIsolateAlive));
216224
}
217225
@@ -222,6 +230,7 @@ interface class $name extends $protocolBase $impls{
222230
/// Note: You cannot call this method after you have called `builder.build`.
223231
static void addToBuilderAsListener($protocolBuilder builder, $args) {
224232
$buildListenerImplementations
233+
builder.addProtocol(\$protocol);
225234
}
226235
227236
/// Builds an object that implements the $originalName protocol. To implement
@@ -233,6 +242,7 @@ interface class $name extends $protocolBase $impls{
233242
static $name implementAsBlocking($args) {
234243
final builder = $protocolBuilder(debugName: '$originalName');
235244
$buildBlockingImplementations
245+
builder.addProtocol(\$protocol);
236246
return $name.castFrom(builder.build(keepIsolateAlive: \$keepIsolateAlive));
237247
}
238248
@@ -243,6 +253,7 @@ interface class $name extends $protocolBase $impls{
243253
/// Note: You cannot call this method after you have called `builder.build`.
244254
static void addToBuilderAsBlocking($protocolBuilder builder, $args) {
245255
$buildBlockingImplementations
256+
builder.addProtocol(\$protocol);
246257
}
247258
''';
248259
}

pkgs/ffigen/test/native_objc_test/protocol_config.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ output:
55
bindings: 'protocol_bindings.dart'
66
objc-bindings: 'protocol_bindings.m'
77
exclude-all-by-default: true
8+
functions:
9+
include:
10+
- getClass
11+
- getClassName
12+
- objc_autoreleasePoolPop
13+
- objc_autoreleasePoolPush
814
objc-interfaces:
915
include:
1016
- ProtocolConsumer

pkgs/ffigen/test/native_objc_test/protocol_test.dart

Lines changed: 140 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import 'dart:isolate';
1212

1313
import 'package:ffi/ffi.dart';
1414
import 'package:objective_c/objective_c.dart';
15+
import 'package:objective_c/src/internal.dart' show getProtocol;
1516
import 'package:test/test.dart';
1617

1718
import '../test_utils.dart';
@@ -24,13 +25,15 @@ typedef VoidMethodBlock = ObjCBlock_ffiVoid_ffiVoid_Int32;
2425
typedef OtherMethodBlock = ObjCBlock_Int32_ffiVoid_Int32_Int32_Int32_Int32;
2526

2627
void main() {
28+
late ProtocolTestObjCLibrary lib;
29+
2730
group('protocol', () {
2831
setUpAll(() {
2932
// TODO(https://github.com/dart-lang/native/issues/1068): Remove this.
3033
DynamicLibrary.open('../objective_c/test/objective_c.dylib');
3134
final dylib = File('test/native_objc_test/objc_test.dylib');
3235
verifySetupFile(dylib);
33-
DynamicLibrary.open(dylib.absolute.path);
36+
lib = ProtocolTestObjCLibrary(DynamicLibrary.open(dylib.absolute.path));
3437
generateBindingsForCoverage('protocol');
3538
});
3639

@@ -126,6 +129,9 @@ void main() {
126129
},
127130
);
128131

132+
expect(MyProtocol.conformsTo(myProtocol), isTrue);
133+
expect(SecondaryProtocol.conformsTo(myProtocol), isFalse);
134+
129135
// Required instance method.
130136
final result = consumer.callInstanceMethod_(myProtocol);
131137
expect(result.toDartString(), 'MyProtocol: Hello from ObjC: 3.14');
@@ -157,6 +163,9 @@ void main() {
157163
final SecondaryProtocol asSecondaryProtocol =
158164
SecondaryProtocol.castFrom(protocolImpl);
159165

166+
expect(MyProtocol.conformsTo(protocolImpl), isTrue);
167+
expect(SecondaryProtocol.conformsTo(protocolImpl), isTrue);
168+
160169
// Required instance method.
161170
final result = consumer.callInstanceMethod_(asMyProtocol);
162171
expect(result.toDartString(), 'ProtocolBuilder: Hello from ObjC: 3.14');
@@ -413,6 +422,71 @@ void main() {
413422
expect(UnusedProtocol.conformsTo(inst), isFalse);
414423
});
415424

425+
test('Threading stress test', () async {
426+
final consumer = ProtocolConsumer();
427+
final completer = Completer<void>();
428+
int count = 0;
429+
430+
final protocolBuilder = ObjCProtocolBuilder();
431+
MyProtocol.voidMethod_.implementAsListener(protocolBuilder, (int x) {
432+
expect(x, 123);
433+
++count;
434+
if (count == 1000) completer.complete();
435+
});
436+
437+
final protocol = protocolBuilder.build();
438+
final MyProtocol asMyProtocol = MyProtocol.castFrom(protocol);
439+
440+
for (int i = 0; i < 1000; ++i) {
441+
consumer.callMethodOnRandomThread_(asMyProtocol);
442+
}
443+
await completer.future;
444+
expect(count, 1000);
445+
});
446+
447+
(NSObject, Pointer<ObjCBlockImpl>) blockRefCountTestInner() {
448+
final pool = lib.objc_autoreleasePoolPush();
449+
final protocolBuilder = ObjCProtocolBuilder();
450+
451+
final block = InstanceMethodBlock.fromFunction(
452+
(Pointer<Void> p, NSString s, double x) => 'Hello'.toNSString());
453+
MyProtocol.instanceMethod_withDouble_
454+
.implementWithBlock(protocolBuilder, block);
455+
final protocol = protocolBuilder.build();
456+
lib.objc_autoreleasePoolPop(pool);
457+
458+
final blockPtr = block.ref.pointer;
459+
460+
// There are 2 references to the block. One owned by the Dart wrapper
461+
// object, and the other owned by the protocol.
462+
doGC();
463+
expect(blockRetainCount(blockPtr), 2);
464+
465+
return (protocol, blockPtr);
466+
}
467+
468+
Pointer<ObjCBlockImpl> blockRefCountTest() {
469+
final (protocol, blockPtr) = blockRefCountTestInner();
470+
471+
// The Dart side block pointer has gone out of scope, but the protocol
472+
// still owns a reference to it.
473+
doGC();
474+
expect(blockRetainCount(blockPtr), 1);
475+
476+
expect(protocol, isNotNull); // Force protocol to stay in scope.
477+
478+
return blockPtr;
479+
}
480+
481+
test('Block ref counting', () {
482+
final blockPtr = blockRefCountTest();
483+
484+
// The protocol object has gone out of scope, so it should be cleaned up.
485+
// So should the block.
486+
doGC();
487+
expect(blockRetainCount(blockPtr), 0);
488+
}, skip: !canDoGC);
489+
416490
test('keepIsolateAlive', () async {
417491
final isolateSendPort = Completer<SendPort>();
418492
final protosCreated = Completer<void>();
@@ -475,5 +549,70 @@ void main() {
475549

476550
receivePort.close();
477551
}, skip: !canDoGC);
552+
553+
test('class disposal, builder first', () {
554+
final pool = lib.objc_autoreleasePoolPush();
555+
ObjCProtocolBuilder? protocolBuilder =
556+
ObjCProtocolBuilder(debugName: 'Foo');
557+
558+
NSObject? protocol = protocolBuilder.build();
559+
final clazz = lib.getClass(protocol);
560+
expect(lib.getClassName(clazz).cast<Utf8>().toDartString(),
561+
startsWith('Foo'));
562+
expect(isValidClass(clazz), isTrue);
563+
lib.objc_autoreleasePoolPop(pool);
564+
565+
protocolBuilder = null;
566+
doGC();
567+
expect(isValidClass(clazz), isTrue);
568+
569+
protocol = null;
570+
doGC();
571+
expect(isValidClass(clazz), isFalse);
572+
}, skip: !canDoGC);
573+
574+
test('class disposal, instance first', () {
575+
final pool = lib.objc_autoreleasePoolPush();
576+
ObjCProtocolBuilder? protocolBuilder =
577+
ObjCProtocolBuilder(debugName: 'Foo');
578+
579+
NSObject? protocol = protocolBuilder.build();
580+
final clazz = lib.getClass(protocol);
581+
expect(lib.getClassName(clazz).cast<Utf8>().toDartString(),
582+
startsWith('Foo'));
583+
expect(isValidClass(clazz), isTrue);
584+
lib.objc_autoreleasePoolPop(pool);
585+
586+
protocolBuilder = null;
587+
doGC();
588+
expect(isValidClass(clazz), isTrue);
589+
590+
protocol = null;
591+
doGC();
592+
expect(isValidClass(clazz), isFalse);
593+
}, skip: !canDoGC);
594+
595+
test('adding more methods after build', () {
596+
final protocolBuilder = ObjCProtocolBuilder();
597+
598+
MyProtocol.addToBuilder(
599+
protocolBuilder,
600+
instanceMethod_withDouble_: (NSString s, double x) {
601+
return 'ProtocolBuilder: ${s.toDartString()}: $x'.toNSString();
602+
},
603+
optionalMethod_: (SomeStruct s) {
604+
return s.y - s.x;
605+
},
606+
);
607+
608+
final protocolImpl = protocolBuilder.build();
609+
610+
expect(
611+
() => SecondaryProtocol.addToBuilder(protocolBuilder,
612+
otherMethod_b_c_d_: (int a, int b, int c, int d) {
613+
return a * b * c * d;
614+
}),
615+
throwsA(isA<StateError>()));
616+
});
478617
});
479618
}

pkgs/ffigen/test/native_objc_test/protocol_test.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@
55
#import <Foundation/NSObject.h>
66
#import <Foundation/NSString.h>
77

8+
const char* getClassName(void* cls);
9+
void* getClass(id object);
10+
void objc_autoreleasePoolPop(void *pool);
11+
void *objc_autoreleasePoolPush();
12+
813
typedef struct {
914
int32_t x;
1015
int32_t y;

pkgs/ffigen/test/native_objc_test/protocol_test.m

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,16 @@
88

99
#include "protocol_test.h"
1010

11+
const char* class_getName(Class cls);
12+
13+
const char* getClassName(void* cls) {
14+
return class_getName((__bridge Class)cls);
15+
}
16+
17+
void* getClass(id object) {
18+
return (__bridge void*)[object class];
19+
}
20+
1121
@implementation ProtocolConsumer : NSObject
1222
- (NSString*)callInstanceMethod:(id<SuperProtocol>)protocol {
1323
return [protocol instanceMethod:@"Hello from ObjC" withDouble:3.14];

pkgs/ffigen/test/native_objc_test/util.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,3 +93,6 @@ int objectRetainCount(Pointer<ObjCObject> object) {
9393
if (!internal_for_testing.isValidClass(clazz)) return 0;
9494
return _getObjectRetainCount(object.cast());
9595
}
96+
97+
bool isValidClass(Pointer<Void> clazz) =>
98+
internal_for_testing.isValidClass(clazz.cast(), forceReloadClasses: true);

pkgs/objective_c/ffigen_c.yaml

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ functions:
2222
- 'protocol_getMethodDescription'
2323
- 'protocol_getName'
2424
- 'newFinalizableHandle'
25-
- 'class_addMethod'
2625
- 'DOBJC_.*'
2726
leaf:
2827
include:
@@ -56,10 +55,7 @@ functions:
5655
'objc_msgSend_stret': 'msgSendStret'
5756
'object_getClass': 'getObjectClass'
5857
'objc_copyClassList': 'copyClassList'
59-
'objc_allocateClassPair': 'allocateClassPair'
60-
'objc_registerClassPair': 'registerClassPair'
6158
'objc_getProtocol': 'getProtocol'
62-
'class_addMethod': 'addMethod'
6359
'protocol_getMethodDescription': 'getMethodDescription'
6460
'protocol_getName': 'getProtocolName'
6561
globals:

pkgs/objective_c/lib/src/c_bindings_generated.dart

Lines changed: 0 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -56,30 +56,6 @@ external ffi.Array<ffi.Pointer<ffi.Void>> NSConcreteMallocBlock;
5656
@ffi.Native<ffi.Array<ffi.Pointer<ffi.Void>>>(symbol: '_NSConcreteStackBlock')
5757
external ffi.Array<ffi.Pointer<ffi.Void>> NSConcreteStackBlock;
5858

59-
@ffi.Native<
60-
ffi.Bool Function(
61-
ffi.Pointer<ObjCObject>,
62-
ffi.Pointer<ObjCSelector>,
63-
ffi.Pointer<ffi.Void>,
64-
ffi.Pointer<ffi.Char>)>(symbol: 'class_addMethod', isLeaf: true)
65-
external bool addMethod(
66-
ffi.Pointer<ObjCObject> cls,
67-
ffi.Pointer<ObjCSelector> name,
68-
ffi.Pointer<ffi.Void> imp,
69-
ffi.Pointer<ffi.Char> types,
70-
);
71-
72-
@ffi.Native<
73-
ffi.Pointer<ObjCObject> Function(
74-
ffi.Pointer<ObjCObject>,
75-
ffi.Pointer<ffi.Char>,
76-
ffi.Size)>(symbol: 'objc_allocateClassPair', isLeaf: true)
77-
external ffi.Pointer<ObjCObject> allocateClassPair(
78-
ffi.Pointer<ObjCObject> superclass,
79-
ffi.Pointer<ffi.Char> name,
80-
int extraBytes,
81-
);
82-
8359
@ffi.Native<ffi.Void Function(ffi.Pointer<ffi.Void>)>(
8460
symbol: 'DOBJC_awaitWaiter')
8561
external void awaitWaiter(
@@ -211,12 +187,6 @@ external ffi.Pointer<ObjCObject> objectRetain(
211187
ffi.Pointer<ObjCObject> object,
212188
);
213189

214-
@ffi.Native<ffi.Void Function(ffi.Pointer<ObjCObject>)>(
215-
symbol: 'objc_registerClassPair', isLeaf: true)
216-
external void registerClassPair(
217-
ffi.Pointer<ObjCObject> cls,
218-
);
219-
220190
@ffi.Native<ffi.Pointer<ObjCSelector> Function(ffi.Pointer<ffi.Char>)>(
221191
symbol: 'sel_registerName', isLeaf: true)
222192
external ffi.Pointer<ObjCSelector> registerName(

pkgs/objective_c/lib/src/internal.dart

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -295,8 +295,8 @@ bool _isValidObject(ObjectPtr ptr) {
295295

296296
final _allClasses = <ObjectPtr>{};
297297

298-
bool _isValidClass(ObjectPtr clazz) {
299-
if (_allClasses.contains(clazz)) return true;
298+
bool _isValidClass(ObjectPtr clazz, {bool forceReloadClasses = false}) {
299+
if (!forceReloadClasses && _allClasses.contains(clazz)) return true;
300300

301301
// If the class is missing from the list, it either means we haven't created
302302
// the set yet, or more classes have been loaded since we created the set, or
@@ -446,5 +446,6 @@ BlockPtr wrapBlockingBlock(
446446
bool blockHasRegisteredClosure(BlockPtr block) =>
447447
_blockClosureRegistry.containsKey(block.ref.target.address);
448448
bool isValidBlock(BlockPtr block) => c.isValidBlock(block);
449-
bool isValidClass(ObjectPtr clazz) => _isValidClass(clazz);
449+
bool isValidClass(ObjectPtr clazz, {bool forceReloadClasses = false}) =>
450+
_isValidClass(clazz, forceReloadClasses: forceReloadClasses);
450451
bool isValidObject(ObjectPtr object) => _isValidObject(object);

0 commit comments

Comments
 (0)