Skip to content

Commit 315b506

Browse files
authored
[objective_c] Add autoReleasePool API (#2658)
1 parent 400c700 commit 315b506

11 files changed

+211
-102
lines changed

pkgs/objective_c/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
- Fix missing `NSNumber` category includes in iOS and macOS `objective_c.m`
55
files.
66
- Add `NSBundle` and `NSNull` to the bindings.
7+
- Add `autoReleasePool` function.
78
- Fix a [bug](https://github.com/dart-lang/native/issues/2627) where
89
`NSMutableDictionary.of` returned a `NSDictionary`.
910

pkgs/objective_c/ffigen_c.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ functions:
4848
'object_getClass': 'getObjectClass'
4949
'objc_copyClassList': 'copyClassList'
5050
'objc_getProtocol': 'getProtocol'
51+
'objc_autoreleasePoolPush': 'autoreleasePoolPush'
52+
'objc_autoreleasePoolPop': 'autoreleasePoolPop'
5153
'protocol_getMethodDescription': 'getMethodDescription'
5254
'protocol_getName': 'getProtocolName'
5355
globals:

pkgs/objective_c/lib/objective_c.dart

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

55
export 'package:pub_semver/pub_semver.dart' show Version;
6+
export 'src/autorelease.dart';
67
export 'src/block.dart';
78
export 'src/c_bindings_generated.dart'
89
show
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Copyright (c) 2025, 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 'c_bindings_generated.dart';
6+
7+
/// Creates an Objective-C autorelease pool, runs [function], then releases the
8+
/// pool.
9+
///
10+
/// ```
11+
/// while (longRunningCondiditon) {
12+
/// // When writing ObjC interop code inside a long running loop, it's a good
13+
/// // idea to use an autorelease pool to clean up autoreleased references.
14+
/// autoReleasePool(() {
15+
/// // Interacting with most ObjC APIs autoreleases a lot of internal refs.
16+
/// final someObjCObject = fooObjCApi.loadNextObject();
17+
/// someObjCObject.greet('Hello'.toNSString());
18+
/// barObjCApi.sendObject(someObjCObject);
19+
/// });
20+
/// }
21+
/// ```
22+
///
23+
/// This is analogous to the Objective-C `@autoreleasepool` block:
24+
/// ```
25+
/// while (longRunningCondiditon) {
26+
/// @autoreleasepool {
27+
/// SomeObjCObject *someObjCObject = [fooObjCApi loadNextObject];
28+
/// [someObjCObject greet:@"Hello"];
29+
/// [barObjCApi sendObject:someObjCObject];
30+
/// }
31+
/// }
32+
/// ```
33+
///
34+
/// [function] is executed synchronously. Do not try to pass an async function
35+
/// here (the [Future] it returns will not be awaited). Objective-C autorelease
36+
/// pools form a strict stack, and allowing async execution gaps inside the pool
37+
/// scope could easily break this nesting, so async functions are not supported.
38+
void autoReleasePool(void Function() function) {
39+
final pool = autoreleasePoolPush();
40+
try {
41+
function();
42+
} finally {
43+
autoreleasePoolPop(pool);
44+
}
45+
}

pkgs/objective_c/lib/src/c_bindings_generated.dart

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,18 @@ external ffi.Array<ffi.Pointer<ffi.Void>> NSConcreteMallocBlock;
4242
@ffi.Native<ffi.Array<ffi.Pointer<ffi.Void>>>(symbol: '_NSConcreteStackBlock')
4343
external ffi.Array<ffi.Pointer<ffi.Void>> NSConcreteStackBlock;
4444

45+
@ffi.Native<ffi.Void Function(ffi.Pointer<ffi.Void>)>(
46+
symbol: 'objc_autoreleasePoolPop',
47+
isLeaf: true,
48+
)
49+
external void autoreleasePoolPop(ffi.Pointer<ffi.Void> pool);
50+
51+
@ffi.Native<ffi.Pointer<ffi.Void> Function()>(
52+
symbol: 'objc_autoreleasePoolPush',
53+
isLeaf: true,
54+
)
55+
external ffi.Pointer<ffi.Void> autoreleasePoolPush();
56+
4557
@ffi.Native<ffi.Void Function(ffi.Pointer<ffi.Void>)>(
4658
symbol: 'DOBJC_awaitWaiter',
4759
)

pkgs/objective_c/lib/src/ns_input_stream.dart

Lines changed: 4 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,6 @@ import '../objective_c.dart';
66

77
import 'objective_c_bindings_generated.dart';
88

9-
@Native<Pointer<Void> Function()>(
10-
isLeaf: true,
11-
symbol: 'objc_autoreleasePoolPush',
12-
)
13-
external Pointer<Void> _autoreleasePoolPush();
14-
15-
@Native<Void Function(Pointer<Void>)>(
16-
isLeaf: true,
17-
symbol: 'objc_autoreleasePoolPop',
18-
)
19-
external void _autoreleasePoolPop(Pointer<Void> pool);
20-
219
extension NSInputStreamStreamExtension on Stream<List<int>> {
2210
/// Return a [NSInputStream] that, when read, will contain the contents of
2311
/// the [Stream].
@@ -46,25 +34,22 @@ extension NSInputStreamStreamExtension on Stream<List<int>> {
4634

4735
final port = ReceivePort();
4836

49-
final DartInputStreamAdapter inputStream;
50-
final DartInputStreamAdapterWeakHolder weakInputStream;
37+
late final DartInputStreamAdapter inputStream;
38+
late final DartInputStreamAdapterWeakHolder weakInputStream;
5139

5240
// Only hold a weak reference to the returned `inputStream` so that there is
5341
// no unbreakable reference cycle between Dart and Objective-C. When the
5442
// `inputStream`'s `dealloc` method is called then it sends this code a
5543
// message saying that it was closed.
56-
final pool = _autoreleasePoolPush();
57-
try {
44+
autoReleasePool(() {
5845
inputStream = DartInputStreamAdapter.inputStreamWithPort(
5946
port.sendPort.nativePort,
6047
);
6148
weakInputStream =
6249
DartInputStreamAdapterWeakHolder.holderWithInputStreamAdapter(
6350
inputStream,
6451
);
65-
} finally {
66-
_autoreleasePoolPop(pool);
67-
}
52+
});
6853

6954
late final StreamSubscription<dynamic> dataSubscription;
7055

pkgs/objective_c/src/objective_c_runtime.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ void objc_release(ObjCObject *object);
2424
ObjCObject *objc_autorelease(ObjCObject *object);
2525
ObjCObject *object_getClass(ObjCObject *object);
2626
ObjCObject** objc_copyClassList(unsigned int* count);
27+
void *objc_autoreleasePoolPush(void);
28+
void objc_autoreleasePoolPop(void *pool);
2729

2830
// The signature of this function is just a placeholder. This function is used
2931
// by every method invocation, and is cast to every signature we need.
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// Copyright (c) 2025, 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+
library;
8+
9+
import 'dart:ffi';
10+
11+
import 'package:objective_c/objective_c.dart';
12+
import 'package:test/test.dart';
13+
14+
import 'util.dart';
15+
16+
void main() {
17+
group('autoReleasePool', () {
18+
setUpAll(() {
19+
// TODO(https://github.com/dart-lang/native/issues/1068): Remove this.
20+
DynamicLibrary.open(testDylib);
21+
});
22+
23+
test('basics', () async {
24+
late Pointer<ObjCObject> pointer;
25+
autoReleasePool(() {
26+
{
27+
final object = NSObject();
28+
pointer = object.ref.autorelease();
29+
}
30+
doGC();
31+
expect(objectRetainCount(pointer), greaterThan(0));
32+
});
33+
34+
doGC();
35+
await Future<void>.delayed(Duration.zero);
36+
doGC();
37+
38+
expect(objectRetainCount(pointer), 0);
39+
});
40+
41+
test('exception safe', () async {
42+
late Pointer<ObjCObject> pointer;
43+
expect(
44+
() => autoReleasePool(() {
45+
{
46+
final object = NSObject();
47+
pointer = object.ref.autorelease();
48+
}
49+
doGC();
50+
expect(objectRetainCount(pointer), greaterThan(0));
51+
throw Exception();
52+
}),
53+
throwsException,
54+
);
55+
56+
doGC();
57+
await Future<void>.delayed(Duration.zero);
58+
doGC();
59+
60+
expect(objectRetainCount(pointer), 0);
61+
});
62+
});
63+
}

pkgs/objective_c/test/ns_input_stream_test.dart

Lines changed: 31 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -325,21 +325,23 @@ void main() {
325325
await completionPort.first;
326326
});
327327
test('with self delegate', () async {
328-
final pool = autoreleasePoolPush();
329-
DartInputStreamAdapter? inputStream =
330-
Stream.fromIterable([
331-
[1, 2, 3],
332-
]).toNSInputStream()
333-
as DartInputStreamAdapter;
334-
335-
expect(inputStream.delegate, inputStream);
336-
337-
final ptr = inputStream.ref.pointer;
338-
autoreleasePoolPop(pool);
328+
late DartInputStreamAdapter? inputStream;
329+
late Pointer<ObjCObject> ptr;
330+
autoReleasePool(() {
331+
inputStream =
332+
Stream.fromIterable([
333+
[1, 2, 3],
334+
]).toNSInputStream()
335+
as DartInputStreamAdapter;
336+
337+
expect(inputStream!.delegate, inputStream);
338+
339+
ptr = inputStream!.ref.pointer;
340+
});
339341
expect(objectRetainCount(ptr), greaterThan(0));
340342

341-
inputStream.open();
342-
inputStream.close();
343+
inputStream!.open();
344+
inputStream!.close();
343345
inputStream = null;
344346

345347
doGC();
@@ -350,22 +352,24 @@ void main() {
350352
});
351353

352354
test('with non-self delegate', () async {
353-
final pool = autoreleasePoolPush();
354-
DartInputStreamAdapter? inputStream =
355-
Stream.fromIterable([
356-
[1, 2, 3],
357-
]).toNSInputStream()
358-
as DartInputStreamAdapter;
359-
360-
inputStream.delegate = NSStreamDelegate.castFrom(NSObject());
361-
expect(inputStream.delegate, isNot(inputStream));
362-
363-
final ptr = inputStream.ref.pointer;
364-
autoreleasePoolPop(pool);
355+
late DartInputStreamAdapter? inputStream;
356+
late Pointer<ObjCObject> ptr;
357+
autoReleasePool(() {
358+
inputStream =
359+
Stream.fromIterable([
360+
[1, 2, 3],
361+
]).toNSInputStream()
362+
as DartInputStreamAdapter;
363+
364+
inputStream!.delegate = NSStreamDelegate.castFrom(NSObject());
365+
expect(inputStream!.delegate, isNot(inputStream));
366+
367+
ptr = inputStream!.ref.pointer;
368+
});
365369
expect(objectRetainCount(ptr), greaterThan(0));
366370

367-
inputStream.open();
368-
inputStream.close();
371+
inputStream!.open();
372+
inputStream!.close();
369373
inputStream = null;
370374

371375
doGC();

0 commit comments

Comments
 (0)