[vm/shared] Introduce NativeCallable.isolateGroupShared

This method allows for synchronous execution of dart callbacks from native code. The execution happens on dart mutator thread, from which dart code can only access isolate-group variables - those which are tagged with .

Bug: https://github.com/dart-lang/sdk/issues/54530
Bug: https://github.com/dart-lang/sdk/issues/56841
Change-Id: Ia1a6b01327be493f003f1eea82e558bb6b147dd3
CoreLibraryReviewExempt: only internal library change
TEST=isolate_group_shared_callback_test
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/422920
Reviewed-by: Slava Egorov <vegorov@google.com>
Commit-Queue: Alexander Aprelev <aam@google.com>
This commit is contained in:
Alexander Aprelev
2025-05-06 13:28:44 -07:00
committed by Commit Queue
parent 7431e885e8
commit 50e0e0d99d
44 changed files with 1225 additions and 121 deletions
+6
View File
@@ -0,0 +1,6 @@
.dart_tool
.packages
pubspec.lock
lib/libfake_http.so
lib/libfake_http.dylib
lib/fake_http.dll
+11
View File
@@ -0,0 +1,11 @@
# Copyright (c) 2023, 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.
shared_library("fake_httpIG") {
sources = [ "lib/fake_http.cc" ]
include_dirs = [ "." ]
if (!is_win) {
ldflags = [ "-rdynamic" ]
}
}
+12
View File
@@ -0,0 +1,12 @@
This is an example that shows how to use `NativeCallable.listener` to interact
with a multi threaded native API.
The native API is a fake HTTP library with some hard coded requests and
responses. To build the dynamic library, run this command:
```bash
c++ -shared -fpic lib/fake_http.cc -lstdc++ -o lib/libfake_http.so
```
On Windows the output library should be `lib/fake_http.dll` and on Mac it should
be `lib/libfake_http.dylib`.
+22
View File
@@ -0,0 +1,22 @@
// Copyright (c) 2025, 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:ffi';
import 'dart:io' show File, Platform;
Uri dylibPath(String name, Uri path) {
if (Platform.isLinux || Platform.isAndroid || Platform.isFuchsia) {
return path.resolve("lib$name.so");
}
if (Platform.isMacOS) return path.resolve("lib$name.dylib");
if (Platform.isWindows) return path.resolve("$name.dll");
throw Exception("Platform not implemented");
}
DynamicLibrary dlopenPlatformSpecific(String name, {List<Uri>? paths}) =>
DynamicLibrary.open(
(paths ?? [Uri()])
.map((path) => dylibPath(name, path).toFilePath())
.firstWhere((lib) => File(lib).existsSync()),
);
+60
View File
@@ -0,0 +1,60 @@
// Copyright (c) 2025, 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.
#include <atomic>
#include <chrono>
#include <cstring>
#include <thread>
#if defined(_WIN32)
#define DART_EXPORT extern "C" __declspec(dllexport)
#else
#define DART_EXPORT \
extern "C" __attribute__((visibility("default"))) __attribute((used))
#endif
constexpr char kExampleRequest[] = R"(
GET / HTTP/1.1
Host: www.example.com
)";
constexpr char kExampleResponse[] = R"(
HTTP/1.1 200 OK
Content-Length: 54
Content-Type: text/html; charset=UTF-8
<html>
<body>
Hello world!
</body>
</html>
)";
DART_EXPORT void http_get(const char* uri, void (*onResponse)(const char*)) {
std::thread([onResponse]() {
std::this_thread::sleep_for(std::chrono::seconds(3));
onResponse(kExampleResponse);
}).detach();
}
std::atomic<bool> stop_requested = false;
std::thread* server = nullptr;
DART_EXPORT void http_start_serving(void (*onRequest)(const char*)) {
server = new std::thread([onRequest]() {
while (!stop_requested) {
std::this_thread::sleep_for(std::chrono::seconds(1));
onRequest(kExampleRequest);
}
});
}
DART_EXPORT void http_stop_serving() {
if (server != nullptr) {
stop_requested = true;
server->join();
delete server;
server = nullptr;
}
}
+142
View File
@@ -0,0 +1,142 @@
// Copyright (c) 2025, 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:convert';
import 'dart:ffi';
import 'dart:isolate';
import 'dart:io';
import 'dart:typed_data';
import 'package:ffi/ffi.dart';
import 'dylib_utils.dart';
// Runs a simple HTTP GET request using a native HTTP library that runs
// the request on a background thread.
Future<String> httpGet(String uri) async {
// Create the NativeCallable.listener.
final completer = Completer<String>();
final ReceivePort rp = ReceivePort()
..listen(
(string) {
completer.complete(string);
},
onError: (e, st) {
print('httpGet receiver get error $e $st');
},
);
final sendPort = rp.sendPort;
final callback = NativeCallable<HttpCallback>.isolateGroupShared((
Pointer<Utf8> responsePointer,
) {
final typedList = responsePointer.cast<Uint8>().asTypedList(
responsePointer.length,
);
final s = utf8.decode(typedList);
sendPort.send(s);
});
// Invoke the native HTTP API. Our example HTTP library runs our GET
// request on a background thread, and calls the callback on that same
// thread when it receives the response.
final uriPointer = uri.toNativeUtf8();
nativeHttpGet(uriPointer, callback.nativeFunction);
calloc.free(uriPointer);
// Wait for the response.
final response = await completer.future;
rp.close();
return response;
}
@pragma('vm:shared')
late int counter;
// Start a HTTP server on a background thread.
ReceivePort httpServe(void Function(String) onRequest) {
counter = 0;
final rp = ReceivePort()
..listen(
(s) {
print('httpServe counter: $counter');
onRequest(s);
},
onError: (e, st) {
print('httpServe receiver get error $e $st');
},
onDone: () {
nativeHttpStopServing();
},
);
final callback = NativeCallable<HttpCallback>.isolateGroupShared((
Pointer<Utf8> requestPointer,
) {
counter++;
final typedList = requestPointer.cast<Uint8>().asTypedList(
requestPointer.length,
);
final s = utf8.decode(typedList);
rp.sendPort.send(s);
});
// Invoke the native function to start the HTTP server. Our example
// HTTP library will start a server on a background thread, and pass
// any requests it receives to out callback.
nativeHttpStartServing(callback.nativeFunction);
return rp;
}
// Load the native functions from a DynamicLibrary.
late final DynamicLibrary dylib = dlopenPlatformSpecific(
'fake_httpIG',
paths: [
Platform.script.resolve('../lib/'),
Uri.file(Platform.resolvedExecutable),
],
);
typedef HttpCallback = Void Function(Pointer<Utf8>);
typedef HttpGetFunction = void Function(
Pointer<Utf8>, Pointer<NativeFunction<HttpCallback>>);
typedef HttpGetNativeFunction = Void Function(
Pointer<Utf8>, Pointer<NativeFunction<HttpCallback>>);
final nativeHttpGet =
dylib.lookupFunction<HttpGetNativeFunction, HttpGetFunction>('http_get');
typedef HttpStartServingFunction = bool Function(
Pointer<NativeFunction<HttpCallback>>);
typedef HttpStartServingNativeFunction = Bool Function(
Pointer<NativeFunction<HttpCallback>>);
final nativeHttpStartServing = dylib
.lookupFunction<HttpStartServingNativeFunction, HttpStartServingFunction>(
'http_start_serving',
);
typedef HttpStopServingFunction = void Function();
typedef HttpStopServingNativeFunction = Void Function();
final nativeHttpStopServing = dylib
.lookupFunction<HttpStopServingNativeFunction, HttpStopServingFunction>(
'http_stop_serving',
);
Future<void> main() async {
print('Sending GET request...');
final response = await httpGet('http://example.com');
print('Received a response: $response');
print('Starting HTTP server...');
final rpServer = httpServe((String request) {
print('Received a request: $request');
});
await Future.delayed(Duration(seconds: 10));
print('All done');
rpServer.close();
}
+14
View File
@@ -0,0 +1,14 @@
name: httpIG_sample
version: 0.0.1
publish_to: none
resolution: workspace
environment:
sdk: ^3.5.0
dependencies:
ffi: ^2.1.0
dev_dependencies:
test: ^1.21.1
+31
View File
@@ -0,0 +1,31 @@
// Copyright (c) 2025, 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.
//
// VMOptions=--experimental-shared-data
// SharedObjects=fake_httpIG
import 'dart:async';
import 'package:test/test.dart';
import 'package:httpIG_sample/http.dart';
Future<void> main() async {
test('httpGet', () async {
final response = await httpGet('http://example.com');
expect(response, contains('Hello world!'));
});
test('httpServe', () async {
final completer = Completer<String>();
final receivePort = httpServe((request) {
if (!completer.isCompleted) {
completer.complete(request);
}
});
final request = await completer.future;
expect(request, contains('www.example.com'));
receivePort.close();
});
}
+1
View File
@@ -16,6 +16,7 @@ ffi/*: SkipByDesign # FFI skips, see ffi.status
[ $arch != x64 || $compiler != dartk || $system != linux || $hot_reload || $hot_reload_rollback ]
ffi/http/test/http_test: SkipByDesign
ffi/httpIG/test/http_test: SkipByDesign
ffi/sqlite/test/sqlite_test: SkipByDesign # FFI not supported or libsqlite3.so not available.
[ $runtime == d8 || $browser ]