diff --git a/BUILD.gn b/BUILD.gn index d6647ee1a55..60545cebcd3 100644 --- a/BUILD.gn +++ b/BUILD.gn @@ -48,6 +48,7 @@ group("runtime") { "runtime/bin:process_test", "runtime/bin:run_vm_tests", "runtime/vm:kernel_platform_files($host_toolchain)", + "samples/ffi/http:fake_http", "utils/dartdev:dartdev", "utils/dds:dds", "utils/kernel-service:kernel-service", diff --git a/samples/ffi/http/.gitignore b/samples/ffi/http/.gitignore new file mode 100644 index 00000000000..a18c42e5cee --- /dev/null +++ b/samples/ffi/http/.gitignore @@ -0,0 +1,6 @@ +.dart_tool +.packages +pubspec.lock +lib/libfake_http.so +lib/libfake_http.dylib +lib/fake_http.dll diff --git a/samples/ffi/http/BUILD.gn b/samples/ffi/http/BUILD.gn new file mode 100644 index 00000000000..b8b98e6fb73 --- /dev/null +++ b/samples/ffi/http/BUILD.gn @@ -0,0 +1,9 @@ +# 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_http") { + sources = [ "lib/fake_http.cc" ] + include_dirs = [ "." ] + ldflags = [ "-rdynamic" ] +} diff --git a/samples/ffi/http/README.md b/samples/ffi/http/README.md new file mode 100644 index 00000000000..023a324632e --- /dev/null +++ b/samples/ffi/http/README.md @@ -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`. diff --git a/samples/ffi/http/lib/dylib_utils.dart b/samples/ffi/http/lib/dylib_utils.dart new file mode 100644 index 00000000000..e3ba4abdb38 --- /dev/null +++ b/samples/ffi/http/lib/dylib_utils.dart @@ -0,0 +1,21 @@ +// 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. + +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? paths}) => + DynamicLibrary.open((paths ?? [Uri()]) + .map((path) => dylibPath(name, path)) + .firstWhere((lib) => File(lib.toFilePath()).existsSync()) + .path); diff --git a/samples/ffi/http/lib/fake_http.cc b/samples/ffi/http/lib/fake_http.cc new file mode 100644 index 00000000000..21a29875bd5 --- /dev/null +++ b/samples/ffi/http/lib/fake_http.cc @@ -0,0 +1,48 @@ +// 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. + +#include +#include +#include +#include + +#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 + + + + Hello world! + + +)"; + +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(strdup(kExampleResponse)); + }).detach(); +} + +DART_EXPORT void http_serve(void (*onRequest)(const char*)) { + std::thread([onRequest]() { + while (true) { + std::this_thread::sleep_for(std::chrono::seconds(1)); + onRequest(strdup(kExampleRequest)); + } + }).detach(); +} diff --git a/samples/ffi/http/lib/http.dart b/samples/ffi/http/lib/http.dart new file mode 100644 index 00000000000..1fde486f72c --- /dev/null +++ b/samples/ffi/http/lib/http.dart @@ -0,0 +1,97 @@ +// 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. + +import 'dart:async'; +import 'dart:ffi'; +import 'dart:io'; + +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 httpGet(String uri) async { + // Create the NativeCallable.listener. + final completer = Completer(); + void onResponse(Pointer responsePointer) { + completer.complete(responsePointer.toDartString()); + calloc.free(responsePointer); + } + + final callback = NativeCallable.listener(onResponse); + + // 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; + + // Remember to close the NativeCallable once the native API is finished + // with it, otherwise this isolate will stay alive indefinitely. + callback.close(); + + return response; +} + +// Start a HTTP server on a background thread. +void httpServe(void Function(String) onRequest) { + // Create the NativeCallable.listener. + void onNativeRequest(Pointer requestPointer) { + onRequest(requestPointer.toDartString()); + calloc.free(requestPointer); + } + + final callback = NativeCallable.listener(onNativeRequest); + + // 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. + nativeHttpServe(callback.nativeFunction); + + // The server will run indefinitely, and the callback needs to stay + // alive for that whole time, so we can't close the callback here. + // But we also don't want the callback to keep the isolate alive + // forever, so we set keepIsolateAlive to false. + callback.keepIsolateAlive = false; +} + +// Load the native functions from a DynamicLibrary. +late final DynamicLibrary dylib = dlopenPlatformSpecific('fake_http', paths: [ + Platform.script.resolve('../lib/'), + Uri.file(Platform.resolvedExecutable), +]); +typedef HttpCallback = Void Function(Pointer); + +typedef HttpGetFunction = void Function( + Pointer, Pointer>); +typedef HttpGetNativeFunction = Void Function( + Pointer, Pointer>); +final nativeHttpGet = + dylib.lookupFunction('http_get'); + +typedef HttpServeFunction = void Function( + Pointer>); +typedef HttpServeNativeFunction = Void Function( + Pointer>); +final nativeHttpServe = dylib + .lookupFunction('http_serve'); + +Future main() async { + print('Sending GET request...'); + final response = await httpGet('http://example.com'); + print('Received a response: $response'); + + print('Starting HTTP server...'); + httpServe((String request) { + print('Received a request: $request'); + }); + + await Future.delayed(Duration(seconds: 10)); + print('All done'); +} diff --git a/samples/ffi/http/pubspec.yaml b/samples/ffi/http/pubspec.yaml new file mode 100644 index 00000000000..2798023736c --- /dev/null +++ b/samples/ffi/http/pubspec.yaml @@ -0,0 +1,12 @@ +name: http +version: 0.0.1 +publish_to: none + +environment: + sdk: '>=3.0.0 <4.0.0' + +dependencies: + ffi: ^2.1.0 + +dev_dependencies: + test: ^1.21.1 diff --git a/samples/ffi/http/test/http_test.dart b/samples/ffi/http/test/http_test.dart new file mode 100644 index 00000000000..509d8dd76c7 --- /dev/null +++ b/samples/ffi/http/test/http_test.dart @@ -0,0 +1,25 @@ +// 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. + +import 'dart:async'; +import 'dart:io'; + +import 'package:ffi/ffi.dart'; +import 'package:test/test.dart'; + +import '../lib/http.dart'; + +Future main() async { + test('httpGet', () async { + final response = await httpGet('http://example.com'); + expect(response, contains('Hello world!')); + }); + + test('httpServe', () async { + final completer = Completer(); + httpServe((request) => completer.complete(request)); + final request = await completer.future; + expect(request, contains('www.example.com')); + }); +} diff --git a/sdk/lib/ffi/ffi.dart b/sdk/lib/ffi/ffi.dart index 45e014c1d4e..a24c7a0f9ea 100644 --- a/sdk/lib/ffi/ffi.dart +++ b/sdk/lib/ffi/ffi.dart @@ -214,6 +214,83 @@ abstract final class NativeCallable { /// /// This callback must be [close]d when it is no longer needed. The [Isolate] /// that created the callback will be kept alive until [close] is called. + /// + /// For example: + /// + /// ```dart + /// import 'dart:async'; + /// import 'dart:ffi'; + /// import 'package:ffi/ffi.dart'; + /// + /// // Runs a simple HTTP GET request using a native HTTP library that runs + /// // the request on a background thread. + /// Future httpGet(String uri) async { + /// // Create the NativeCallable.listener. + /// final completer = Completer(); + /// void onResponse(Pointer responsePointer) { + /// completer.complete(responsePointer.toDartString()); + /// calloc.free(responsePointer); + /// } + /// final callback = NativeCallable.listener(onResponse); + /// + /// // 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; + /// + /// // Remember to close the NativeCallable once the native API is finished + /// // with it, otherwise this isolate will stay alive indefinitely. + /// callback.close(); + /// + /// return response; + /// } + /// + /// // Start a HTTP server on a background thread. + /// void httpServe(void Function(String) onRequest) { + /// // Create the NativeCallable.listener. + /// void onNativeRequest(Pointer requestPointer) { + /// onRequest(requestPointer.toDartString()); + /// calloc.free(requestPointer); + /// } + /// final callback = NativeCallable.listener(onNativeRequest); + /// + /// // 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. + /// nativeHttpServe(callback.nativeFunction); + /// + /// // The server will run indefinitely, and the callback needs to stay + /// // alive for that whole time, so we can't close the callback here. + /// // But we also don't want the callback to keep the isolate alive + /// // forever, so we set keepIsolateAlive to false. + /// callback.keepIsolateAlive = false; + /// } + /// + /// // Load the native functions from a DynamicLibrary. + /// late final DynamicLibrary dylib = DynamicLibrary.process(); + /// typedef HttpCallback = Void Function(Pointer); + /// + /// typedef HttpGetFunction = void Function( + /// Pointer, Pointer>); + /// typedef HttpGetNativeFunction = Void Function( + /// Pointer, Pointer>); + /// final nativeHttpGet = + /// dylib.lookupFunction( + /// 'http_get'); + /// + /// typedef HttpServeFunction = void Function( + /// Pointer>); + /// typedef HttpServeNativeFunction = Void Function( + /// Pointer>); + /// final nativeHttpServe = + /// dylib.lookupFunction( + /// 'http_serve'); + /// ``` factory NativeCallable.listener( @DartRepresentationOf("T") Function callback) { throw UnsupportedError("NativeCallable cannot be constructed dynamically.");