From baaf5e1116104e24673399d1fd0b772de596e166 Mon Sep 17 00:00:00 2001 From: Liam Appelbe Date: Thu, 28 Sep 2023 22:41:42 +0000 Subject: [PATCH] [ffi] Reland the iNativeCallable.listener example test. The documentation changes already relanded. This is just relanding the example. Patchset 1 is a pure reland. Other patchsets are fixes. Original CL: https://dart-review.googlesource.com/c/sdk/+/326580 Change-Id: I04d24d63f08a351db7a6e43f331904274e28e2d5 Bug: https://github.com/dart-lang/sdk/issues/53435 TEST=samples/ffi/http/test/http_test.dart Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/328681 Commit-Queue: Liam Appelbe Reviewed-by: Ryan Macnak --- BUILD.gn | 1 + samples/ffi/http/.gitignore | 6 ++ samples/ffi/http/BUILD.gn | 9 ++ samples/ffi/http/README.md | 12 +++ samples/ffi/http/lib/dylib_utils.dart | 20 +++++ samples/ffi/http/lib/fake_http.cc | 48 +++++++++++ samples/ffi/http/lib/http.dart | 97 ++++++++++++++++++++++ samples/ffi/http/pubspec.yaml | 12 +++ samples/ffi/http/test/http_test.dart | 25 ++++++ tools/linux_dist_support/create_tarball.py | 1 - 10 files changed, 230 insertions(+), 1 deletion(-) create mode 100644 samples/ffi/http/.gitignore create mode 100644 samples/ffi/http/BUILD.gn create mode 100644 samples/ffi/http/README.md create mode 100644 samples/ffi/http/lib/dylib_utils.dart create mode 100644 samples/ffi/http/lib/fake_http.cc create mode 100644 samples/ffi/http/lib/http.dart create mode 100644 samples/ffi/http/pubspec.yaml create mode 100644 samples/ffi/http/test/http_test.dart 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..0425d78b8d6 --- /dev/null +++ b/samples/ffi/http/lib/dylib_utils.dart @@ -0,0 +1,20 @@ +// 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).toFilePath()) + .firstWhere((lib) => File(lib).existsSync())); 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/tools/linux_dist_support/create_tarball.py b/tools/linux_dist_support/create_tarball.py index b519c43265d..059aecbfd8a 100755 --- a/tools/linux_dist_support/create_tarball.py +++ b/tools/linux_dist_support/create_tarball.py @@ -42,7 +42,6 @@ versiondir = '' ignoredPaths = [ 'buildtools/linux-x64/go', 'buildtools/linux-x64/rust', - 'samples', 'third_party/7zip', 'third_party/android_tools', 'third_party/clang',