-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
Lightweight communication of typed data between long-lived isolates #52577
Comments
I don't think A more realistic approach would be to have a If you don't worry about memory safety, or think you can handle it, another alternative is using You should be able to allocate bytes external to the heap, e.g., using import "dart:typed_data";
import "dart:isolate";
import "dart:ffi";
import "package:ffi/ffi.dart";
void main() async {
var allocation = calloc.allocate<Uint8>(32);
var bytes = allocation.asTypedList(32);
print("1: $bytes");
for (var i = 0; i < bytes.length; i += 2) {
bytes[i] = 1;
}
print("1: $bytes");
var address = allocation.address;
await Isolate.run(() {
Uint8List bytes = Pointer<Uint8>.fromAddress(address).asTypedList(32);
print("2: $bytes");
for (var i = 0; i < bytes.length; i += 2) {
bytes[i + 1] = bytes[i] + 1;
}
print("2: $bytes");
});
print("2: $bytes");
calloc.free(allocation);
} Efficient access, efficient transfer, memory safe. Pick two. |
I think something like a abstract class TransferableUint8Buffer {
/// If [startingLength] is passed, allocate that many bytes to begin with.
TransferableUint8Buffer([int startingLength]);
void add(int value);
void addAll(List<int> values);
void addRange(List<int> values, int start, int end);
TransferableTypedData toData();
} ...but I don't think this can be implemented in user code today without requiring at least some degree of additional copying or allocation. |
//cc @aam |
This is somewhat problematic for a number of reasons:
This can be implemented using Though one can go a step further and actually create a safely shareable deeply immutable import 'dart:ffi';
import 'dart:io';
import 'dart:typed_data';
import 'dart:isolate';
main() async {
final Uint8List data = buildSharableTypedData();
final result = await Isolate.run(() => data);
if (!identical(data, result)) throw 'Should share typed data.';
}
Uint8List buildSharableTypedData() {
final buffer = TransferableUint8Buffer();
for (int i = 0; i < 1000; ++i) {
buffer.add(i % 256);
}
return buffer.toData();
}
class TransferableUint8Buffer {
late Pointer<Uint8> pointer;
late Uint8List bytes;
late int capacity;
late int length;
TransferableUint8Buffer([this.capacity = 8]) {
pointer = allocate(capacity).cast();
bytes = pointer.asTypedList(capacity);
length = 0;
}
void add(int value) {
_ensureCapacity(length + 1);
bytes[length++] = value;
}
void addAll(List<int> values) {
addRange(values, 0, values.length);
}
void addRange(List<int> values, int start, int end) {
final int extra = end - start;
_ensureCapacity(length + extra);
for (int i = start; i < end; ++i) {
bytes[length++] = values[i];
}
}
Uint8List toData() {
const int Dart_TypedData_kUint8 = 2;
return createUnmodifiableTypedData(Dart_TypedData_kUint8, pointer, length,
pointer, capacity, freePointer.cast());
}
void _ensureCapacity(int newLen) {
if (capacity < newLen) {
final oldPointer = pointer;
final oldBytes = bytes;
capacity = 1 << (newLen.bitLength);
pointer = allocate(capacity).cast();
bytes = pointer.asTypedList(capacity);
bytes.setRange(0, length, oldBytes);
free(oldPointer);
}
}
}
@Native<
Handle Function(Int, Pointer<Uint8>, IntPtr, Pointer<Uint8>, IntPtr,
Pointer<Void>)>(
symbol: "Dart_NewUnmodifiableExternalTypedDataWithFinalizer")
external Uint8List createUnmodifiableTypedData(int type, Pointer<Uint8> data,
int length, Pointer<Uint8> peer, int externalSize, Pointer<Void> callback);
final DynamicLibrary stdlib = Platform.isWindows
? DynamicLibrary.open('ole32.dll')
: DynamicLibrary.process();
final allocate =
stdlib.lookupFunction<Pointer Function(IntPtr), Pointer Function(int)>(
Platform.isWindows ? 'CoTaskMemAlloc' : 'malloc', isLeaf: true);
final freePointer = stdlib.lookup<NativeFunction<Void Function(Pointer)>>(
Platform.isWindows ? 'CoTaskMemFree' : 'free');
final free = freePointer.asFunction<void Function(Pointer)>(isLeaf: true); |
Maybe some addition to the above: The benefit is that the final buffer doesn't have to be copied anymore but can be directly used to create an immutable typed data wrapper. That pays of if the data is very large. Though it may be slow for low amounts of data, as one has to leave dart code to perform allocation/freeing/typed-data-creation in C code. See related issues that may avoid this issue: |
Is there anymore actionable items to be done for this issue. Using the native buffer appears to solve the immediate problem reported here native_synchronization . I intend to close this issue if I do not hear back. |
@a-siva The documentation for the native_synchronization package doesn't mention anything about not copying data. If that's a guarantee it offers and that guarantee is added to the documentation, then I suppose that satisfies my own use-case—although I suspect others may want a way to pass around data without copying in asynchronous contexts as well as synchronous ones. If that package doesn't offer a zero-copy guarantee, then this is still very much an issue. |
dart-lang/language#124 was closed with
Isolate.exit()
's ability to efficiently send a final message as the stated solution. However, this doesn't help the use-case where an isolate is expected to be long-lived. For example, I'm working on making the Sass embedded compiler more parallel using isolates, which involves passing encoded protocol buffers from worker isolates through the main isolate and from there to another program. We want to be able to pass messages during the compilation process, not merely at the end, and we want to avoid copying the encoded buffers when sending them across isolate boundaries.These buffers are effectively immutable after creation—that is, there's a clear point before they're sent across isolate boundaries that we can guarantee their contents and length will never be modified. As a straw suggestion, adding a
TypedData.freeze()
that marks the object as immutable and thus lightweight-transferable would solve this use-case.The text was updated successfully, but these errors were encountered: