Skip to content

Commit e5dee44

Browse files
authored
Use NSProxy to implement ObjC protocols (#1144)
1 parent 413dd27 commit e5dee44

24 files changed

+2135
-1230
lines changed

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ class ObjCBuiltInFunctions {
2727

2828
// Keep in sync with pkgs/objective_c/ffigen_objc.yaml.
2929
static const builtInInterfaces = {
30+
'DartProxy',
31+
'DartProxyBuilder',
3032
'NSArray',
3133
'NSCharacterSet',
3234
'NSCoder',
@@ -49,6 +51,7 @@ class ObjCBuiltInFunctions {
4951
'NSNumber',
5052
'NSObject',
5153
'NSProgress',
54+
'NSProxy',
5255
'NSSet',
5356
'NSString',
5457
'NSURL',

pkgs/ffigen/test/native_objc_test/is_instance_test.dart

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
// BSD-style license that can be found in the LICENSE file.
44

55
// Objective C support is only available on mac.
6-
76
@TestOn('mac-os')
87

98
import 'dart:ffi';
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
name: ProtocolTestObjCLibrary
2+
description: 'Tests implementing protocols'
3+
language: objc
4+
output: 'protocol_bindings.dart'
5+
exclude-all-by-default: true
6+
objc-interfaces:
7+
include:
8+
- ProtocolConsumer
9+
- ObjCProtocolImpl
10+
- ObjCProtocolImplMissingMethod
11+
typedefs:
12+
include:
13+
- InstanceMethodBlock
14+
- OptMethodBlock
15+
- VoidMethodBlock
16+
- OtherMethodBlock
17+
functions:
18+
include:
19+
- forceCodeGenOfInstanceMethodBlock
20+
- forceCodeGenOfOptMethodBlock
21+
- forceCodeGenOfVoidMethodBlock
22+
- forceCodeGenOfOtherMethodBlock
23+
headers:
24+
entry-points:
25+
- 'protocol_test.m'
26+
preamble: |
27+
// ignore_for_file: camel_case_types, non_constant_identifier_names, unnecessary_non_null_assertion, unused_element, unused_field
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
// Objective C support is only available on mac.
6+
@TestOn('mac-os')
7+
8+
import 'dart:async';
9+
import 'dart:ffi';
10+
import 'dart:io';
11+
12+
import 'package:objective_c/objective_c.dart';
13+
import 'package:test/test.dart';
14+
15+
import '../test_utils.dart';
16+
import 'protocol_bindings.dart';
17+
import 'util.dart';
18+
19+
void main() {
20+
group('protocol', () {
21+
setUpAll(() {
22+
logWarnings();
23+
// TODO(https://github.com/dart-lang/native/issues/1068): Remove this.
24+
DynamicLibrary.open('../objective_c/test/objective_c.dylib');
25+
final dylib = File('test/native_objc_test/protocol_test.dylib');
26+
verifySetupFile(dylib);
27+
DynamicLibrary.open(dylib.absolute.path);
28+
generateBindingsForCoverage('protocol');
29+
});
30+
31+
group('ObjC implementation', () {
32+
test('Method implementation', () {
33+
final protoImpl = ObjCProtocolImpl.new1();
34+
final consumer = ProtocolConsumer.new1();
35+
36+
// Required instance method.
37+
final result = consumer.callInstanceMethod_(protoImpl);
38+
expect(result.toString(), 'ObjCProtocolImpl: Hello from ObjC: 3.14');
39+
40+
// Optional instance method.
41+
final intResult = consumer.callOptionalMethod_(protoImpl);
42+
expect(intResult, 579);
43+
44+
// Required instance method from secondary protocol.
45+
final otherIntResult = consumer.callOtherMethod_(protoImpl);
46+
expect(otherIntResult, 10);
47+
});
48+
49+
test('Unimplemented method', () {
50+
final protoImpl = ObjCProtocolImplMissingMethod.new1();
51+
final consumer = ProtocolConsumer.new1();
52+
53+
// Optional instance method, not implemented.
54+
final intResult = consumer.callOptionalMethod_(protoImpl);
55+
expect(intResult, -999);
56+
});
57+
});
58+
59+
group('Manual DartProxy implementation', () {
60+
test('Method implementation', () {
61+
final proxyBuilder = DartProxyBuilder.new1();
62+
final consumer = ProtocolConsumer.new1();
63+
final proto = getProtocol('MyProtocol');
64+
final secondProto = getProtocol('SecondaryProtocol');
65+
66+
final sel = registerName('instanceMethod:withDouble:');
67+
final signature = getProtocolMethodSignature(proto, sel,
68+
isRequired: true, isInstance: true);
69+
final block = DartInstanceMethodBlock.fromFunction(
70+
(Pointer<Void> p, NSString s, double x) {
71+
return 'DartProxy: $s: $x'.toNSString();
72+
});
73+
proxyBuilder.implementMethod_withSignature_andBlock_(
74+
sel, signature!, block.pointer.cast());
75+
76+
final optSel = registerName('optionalMethod:');
77+
final optSignature = getProtocolMethodSignature(proto, optSel,
78+
isRequired: false, isInstance: true);
79+
final optBlock =
80+
DartOptMethodBlock.fromFunction((Pointer<Void> p, SomeStruct s) {
81+
return s.y - s.x;
82+
});
83+
proxyBuilder.implementMethod_withSignature_andBlock_(
84+
optSel, optSignature!, optBlock.pointer.cast());
85+
86+
final otherSel = registerName('otherMethod:b:c:d:');
87+
final otherSignature = getProtocolMethodSignature(secondProto, otherSel,
88+
isRequired: true, isInstance: true);
89+
final otherBlock = DartOtherMethodBlock.fromFunction(
90+
(Pointer<Void> p, int a, int b, int c, int d) {
91+
return a * b * c * d;
92+
});
93+
proxyBuilder.implementMethod_withSignature_andBlock_(
94+
otherSel, otherSignature!, otherBlock.pointer.cast());
95+
96+
final proxy = DartProxy.newFromBuilder_(proxyBuilder);
97+
98+
// Required instance method.
99+
final result = consumer.callInstanceMethod_(proxy);
100+
expect(result.toString(), "DartProxy: Hello from ObjC: 3.14");
101+
102+
// Optional instance method.
103+
final intResult = consumer.callOptionalMethod_(proxy);
104+
expect(intResult, 333);
105+
106+
// Required instance method from secondary protocol.
107+
final otherIntResult = consumer.callOtherMethod_(proxy);
108+
expect(otherIntResult, 24);
109+
});
110+
111+
test('Unimplemented method', () {
112+
final proxyBuilder = DartProxyBuilder.new1();
113+
final consumer = ProtocolConsumer.new1();
114+
final proxy = DartProxy.newFromBuilder_(proxyBuilder);
115+
116+
// Optional instance method, not implemented.
117+
final intResult = consumer.callOptionalMethod_(proxy);
118+
expect(intResult, -999);
119+
});
120+
121+
test('Threading stress test', () async {
122+
final proxyBuilder = DartProxyBuilder.new1();
123+
final consumer = ProtocolConsumer.new1();
124+
final proto = getProtocol('MyProtocol');
125+
final completer = Completer<void>();
126+
int count = 0;
127+
128+
final sel = registerName('voidMethod:');
129+
final signature = getProtocolMethodSignature(proto, sel,
130+
isRequired: false, isInstance: true);
131+
final block = DartVoidMethodBlock.listener((Pointer<Void> p, int x) {
132+
expect(x, 123);
133+
++count;
134+
if (count == 1000) completer.complete();
135+
});
136+
proxyBuilder.implementMethod_withSignature_andBlock_(
137+
sel, signature!, block.pointer.cast());
138+
139+
final proxy = DartProxy.newFromBuilder_(proxyBuilder);
140+
141+
for (int i = 0; i < 1000; ++i) {
142+
consumer.callMethodOnRandomThread_(proxy);
143+
}
144+
await completer.future;
145+
expect(count, 1000);
146+
});
147+
148+
(DartProxy, Pointer<ObjCBlock>) blockRefCountTestInner() {
149+
final proxyBuilder = DartProxyBuilder.new1();
150+
final proto = getProtocol('MyProtocol');
151+
152+
final sel = registerName('instanceMethod:withDouble:');
153+
final signature = getProtocolMethodSignature(proto, sel,
154+
isRequired: true, isInstance: true);
155+
final block = DartInstanceMethodBlock.fromFunction(
156+
(Pointer<Void> p, NSString s, double x) => 'Hello'.toNSString());
157+
proxyBuilder.implementMethod_withSignature_andBlock_(
158+
sel, signature!, block.pointer.cast());
159+
160+
final proxy = DartProxy.newFromBuilder_(proxyBuilder);
161+
162+
final proxyPtr = proxy.pointer;
163+
final blockPtr = block.pointer;
164+
165+
// There are 2 references to the block. One owned by the Dart wrapper
166+
// object, and the other owned by the proxy. The method signature is
167+
// also an ObjC object, so the same is true for it.
168+
doGC();
169+
expect(objectRetainCount(proxyPtr), 1);
170+
expect(blockRetainCount(blockPtr), 2);
171+
172+
return (proxy, blockPtr);
173+
}
174+
175+
(Pointer<ObjCObject>, Pointer<ObjCBlock>) blockRefCountTest() {
176+
final (proxy, blockPtr) = blockRefCountTestInner();
177+
final proxyPtr = proxy.pointer;
178+
179+
// The Dart side block pointer has gone out of scope, but the proxy
180+
// still owns a reference to it. Same for the signature.
181+
doGC();
182+
expect(objectRetainCount(proxyPtr), 1);
183+
expect(blockRetainCount(blockPtr), 1);
184+
185+
return (proxyPtr, blockPtr);
186+
}
187+
188+
test('Block ref counting', () {
189+
final (proxyPtr, blockPtr) = blockRefCountTest();
190+
191+
// The proxy object has gone out of scope, so it should be cleaned up.
192+
// So should the block and the signature.
193+
doGC();
194+
expect(objectRetainCount(proxyPtr), 0);
195+
expect(blockRetainCount(blockPtr), 0);
196+
});
197+
});
198+
});
199+
}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
#import <dispatch/dispatch.h>
6+
#import <Foundation/NSObject.h>
7+
#import <Foundation/NSString.h>
8+
9+
#include "util.h"
10+
11+
typedef struct {
12+
int32_t x;
13+
int32_t y;
14+
} SomeStruct;
15+
16+
@protocol MyProtocol<NSObject>
17+
18+
@required
19+
- (NSString*)instanceMethod:(NSString*)s withDouble:(double)x;
20+
21+
@optional
22+
- (int32_t)optionalMethod:(SomeStruct)s;
23+
24+
@optional
25+
- (void)voidMethod:(int32_t)x;
26+
27+
@end
28+
29+
30+
@protocol SecondaryProtocol<NSObject>
31+
32+
@required
33+
- (int32_t)otherMethod:(int32_t)a b:(int32_t)b c:(int32_t)c d:(int32_t)d;
34+
35+
@end
36+
37+
38+
@interface ProtocolConsumer : NSObject
39+
- (NSString*)callInstanceMethod:(id<MyProtocol>)proto;
40+
- (int32_t)callOptionalMethod:(id<MyProtocol>)proto;
41+
- (int32_t)callOtherMethod:(id<SecondaryProtocol>)proto;
42+
- (void)callMethodOnRandomThread:(id<SecondaryProtocol>)proto;
43+
@end
44+
45+
@implementation ProtocolConsumer : NSObject
46+
- (NSString*)callInstanceMethod:(id<MyProtocol>)proto {
47+
return [proto instanceMethod:@"Hello from ObjC" withDouble:3.14];
48+
}
49+
50+
- (int32_t)callOptionalMethod:(id<MyProtocol>)proto {
51+
if ([proto respondsToSelector:@selector(optionalMethod:)]) {
52+
SomeStruct s = {123, 456};
53+
return [proto optionalMethod:s];
54+
} else {
55+
return -999;
56+
}
57+
}
58+
59+
- (int32_t)callOtherMethod:(id<SecondaryProtocol>)proto {
60+
return [proto otherMethod:1 b:2 c:3 d:4];
61+
}
62+
63+
- (void)callMethodOnRandomThread:(id<MyProtocol>)proto {
64+
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
65+
[proto voidMethod:123];
66+
});
67+
}
68+
@end
69+
70+
71+
@interface ObjCProtocolImpl : NSObject<MyProtocol, SecondaryProtocol>
72+
@end
73+
74+
@implementation ObjCProtocolImpl
75+
- (NSString *)instanceMethod:(NSString *)s withDouble:(double)x {
76+
return [NSString stringWithFormat:@"ObjCProtocolImpl: %@: %.2f", s, x];
77+
}
78+
79+
- (int32_t)optionalMethod:(SomeStruct)s {
80+
return s.x + s.y;
81+
}
82+
83+
- (int32_t)otherMethod:(int32_t)a b:(int32_t)b c:(int32_t)c d:(int32_t)d {
84+
return a + b + c + d;
85+
}
86+
87+
@end
88+
89+
90+
@interface ObjCProtocolImplMissingMethod : NSObject<MyProtocol>
91+
@end
92+
93+
@implementation ObjCProtocolImplMissingMethod
94+
- (NSString *)instanceMethod:(NSString *)s withDouble:(double)x {
95+
return @"ObjCProtocolImplMissingMethod";
96+
}
97+
@end
98+
99+
100+
// TODO(https://github.com/dart-lang/native/issues/1040): Delete these.
101+
typedef NSString* (^InstanceMethodBlock)(void*, NSString*, double);
102+
void forceCodeGenOfInstanceMethodBlock(InstanceMethodBlock block);
103+
typedef int32_t (^OptMethodBlock)(void*, SomeStruct);
104+
void forceCodeGenOfOptMethodBlock(OptMethodBlock block);
105+
typedef void (^VoidMethodBlock)(void*, int32_t);
106+
void forceCodeGenOfVoidMethodBlock(VoidMethodBlock block);
107+
typedef int32_t (^OtherMethodBlock)(void*, int32_t a, int32_t b, int32_t c, int32_t d);
108+
void forceCodeGenOfOtherMethodBlock(OtherMethodBlock block);

pkgs/objective_c/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
*.pyc
55
*.swp
66
*.dylib
7+
*.o
78
.DS_Store
89
.atom/
910
.buildlog/

pkgs/objective_c/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
## 1.1.0-wip
2+
3+
- Add `DartProxy`, which is an implementation of `NSProxy` that enables
4+
implementing ObjC protocols from Dart. Also adds `DartProxyBuilder` for
5+
constructing `DartProxy`.
6+
17
## 1.0.1
28

39
- Mention experimental status in readme.

pkgs/objective_c/example/pubspec.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ packages:
137137
path: ".."
138138
relative: true
139139
source: path
140-
version: "0.0.1-wip"
140+
version: "1.0.0"
141141
path:
142142
dependency: transitive
143143
description:

0 commit comments

Comments
 (0)