blob: 021d815c55d272fe3cd1cf634858b118c3934af2 [file] [edit]
// Copyright (c) 2026, 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.
import 'dart:async';
import 'dart:ffi';
import 'dart:isolate';
import 'autorelease.dart';
import 'globals.dart';
import 'ns_data.dart';
import 'ns_dictionary.dart';
import 'ns_string.dart';
import 'objective_c_bindings_generated.dart';
extension NSInputStreamStreamExtension on Stream<List<int>> {
/// Return a [NSInputStream] that, when read, will contain the contents of
/// the [Stream].
///
/// > [!IMPORTANT]
/// > `NSInputStream.read` must be called from a different thread or [Isolate]
/// > than the one that calls [toNSInputStream]. Otherwise,
/// > `NSInputStream.read` will deadlock waiting for data to be added from the
/// > [Stream].
///
/// > [!IMPORTANT]
/// > `toNSInputStream` creates a reference cycle between Dart and
/// > Objective-C. Unless this cycle is broken, the [Isolate] calling
/// > `toNSInputStream` will never exit. The cycle can be broken by calling
/// > `NSInputStream.close` or releasing the `NSInputStream` using
/// > `NSInputStream.ref.release()`.
NSInputStream toNSInputStream() {
// Eagerly add data until `maxReadAheadSize` is buffered.
const maxReadAheadSize = 4096;
final port = ReceivePort();
late final DartInputStreamAdapter inputStream;
// When `adapter` is accessed from `weakInputStream`, it returns a
// `NSInputStream` loaded as a weak reference.
//
// Loading a weak reference adds the object to the current autorelease pool
// block [1].
//
// Therefore, it is important that every reference to
// `weakInputStream.adapter` is made in a short-lived autorelease pool
// block so that the weak reference is released as soon as possible and no
// long-lived reference cycle is created between Dart and Objective-C.
//
// [1] https://developer.apple.com/documentation/objectivec/objc_loadweak(_:)
late final DartInputStreamAdapterWeakHolder weakInputStream;
// Only hold a weak reference to the returned `inputStream` so that there is
// no unbreakable reference cycle between Dart and Objective-C. When the
// `inputStream`'s `dealloc` method is called then it sends this code a
// message saying that it was closed.
autoReleasePool(() {
inputStream = DartInputStreamAdapter.inputStreamWithPort(
port.sendPort.nativePort,
);
weakInputStream =
DartInputStreamAdapterWeakHolder.holderWithInputStreamAdapter(
inputStream,
);
});
late final StreamSubscription<dynamic> dataSubscription;
dataSubscription = listen(
(data) {
autoReleasePool(() {
final inputStream = weakInputStream.adapter;
if (inputStream.addData(data.toNSData()) > maxReadAheadSize) {
dataSubscription.pause();
}
inputStream.ref.release();
});
},
onError: (Object e) {
autoReleasePool(() {
final inputStream = weakInputStream.adapter;
final d = NSMutableDictionary();
d.asDart()[NSLocalizedDescriptionKey] = e.toString().toNSString();
inputStream.setError(
NSError.errorWithDomain(
'DartError'.toNSString(),
code: 0,
userInfo: d,
),
);
inputStream.ref.release();
});
port.close();
},
onDone: () {
autoReleasePool(() {
final inputStream = weakInputStream.adapter;
inputStream.setDone();
inputStream.ref.release();
});
port.close();
},
cancelOnError: true,
);
dataSubscription.pause();
port.listen(
(count) {
// -1 indicates that the `NSInputStream` is closed. All other values
// indicate that the `NSInputStream` needs more data.
//
// If [DartInputStreamAdapter.setError] or
// [DartInputStreamAdapter.setDone] is called then the close message
// (-1) will not be sent when the input stream is closed.
if (count == -1) {
port.close();
} else {
dataSubscription.resume();
}
},
onDone: () {
unawaited(dataSubscription.cancel());
},
);
return inputStream;
}
}