diff --git a/CHANGELOG.md b/CHANGELOG.md index b59dc68e..131ace1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## 6.13.0 + +- The `get_dart_signal_receiver` function now returns `Result`. You need to use `unwrap` or `?` to retrieve Dart signal receivers from the generated message structs. +- You now need to manually provide the generated `assignRustSignal` to the `initializeRust` function, which can be imported using `import 'package:rinf/rinf.dart';` in Dart. +- The tokio runtime will now default to being single-threaded. To use a multi-threaded tokio runtime, enable the new `rt-multi-thread` crate feature. +- By default, backtraces will be hidden in the CLI, with only the error message being printed. To display the Rust backtrace, enable the new `backtrace` crate feature. + ## 6.12.1 - Fixed linefeed problem in published files. diff --git a/documentation/docs/configuration.md b/documentation/docs/configuration.md index 692c3cec..8e36f47b 100644 --- a/documentation/docs/configuration.md +++ b/documentation/docs/configuration.md @@ -27,5 +27,5 @@ Customizing the behavior of the Rinf crate is possible through its crate feature rinf = { version = "0.0.0", features = ["feature-name"] } ``` -- `multi-worker`: Starts a worker thread for each CPU core available on the system within the `tokio` runtime by enabling its `rt-multi-thread` feature. By default, the `tokio` runtime uses only one thread. Enabling this feature allows the `tokio` runtime to utilize all the cores on your computer. This feature does not affect applications on the web platform. +- `rt-multi-thread`: Starts a worker thread for each CPU core available on the system within the `tokio` runtime by enabling its `rt-multi-thread` feature. By default, the `tokio` runtime uses only one thread. Enabling this feature allows the `tokio` runtime to utilize all the cores on your computer. This feature does not affect applications on the web platform. - `show-backtrace`: Prints the full backtrace in the CLI when a panic occurs in debug mode. In general, backtrace is not very helpful when debugging async apps, so consider using [`tracing`](https://crates.io/crates/tracing) for logging purposes. Note that this feature does not affect debugging on the web platform. diff --git a/documentation/docs/frequently-asked-questions.md b/documentation/docs/frequently-asked-questions.md index b404a74b..8c3a6a28 100644 --- a/documentation/docs/frequently-asked-questions.md +++ b/documentation/docs/frequently-asked-questions.md @@ -266,7 +266,7 @@ Here are the current constraints of the `wasm32-unknown-unknown` target: - Various features of `std::net` are not available. Consider using `reqwest` crate instead. `reqwest` supports `wasm32-unknown-unknown` and relies on JavaScript to perform network communications. - `std::thread::spawn` doesn't work. Consider using `tokio_with_wasm::task::spawn_blocking` instead. - Several features of `std::time::Instant` are unimplemented. Consider using `chrono` as an alternative. `chrono` supports `wasm32-unknown-unknown` and relies on JavaScript to obtain system time. -- In case of a panic in an asynchronous Rust task, it aborts and throws a JavaScript `RuntimeError` [which Rust cannot catch](https://stackoverflow.com/questions/59426545/rust-paniccatch-unwind-no-use-in-webassembly). A recommended practice is to replace `.unwrap` with `.expect` or handle errors with `Err` instances. +- In case of a panic in an asynchronous Rust task, it aborts and throws a JavaScript `RuntimeError` [which Rust cannot catch](https://stackoverflow.com/questions/59426545/rust-paniccatch-unwind-no-use-in-webassembly). A recommended practice is to handle errors with `Err` instances. ### My app failed to load dynamic library diff --git a/documentation/docs/graceful-shutdown.md b/documentation/docs/graceful-shutdown.md index 42bd8447..33b1bfd4 100644 --- a/documentation/docs/graceful-shutdown.md +++ b/documentation/docs/graceful-shutdown.md @@ -1,46 +1,49 @@ # Graceful Shutdown -When the Flutter app is closed, the entire `tokio` async runtime on the Rust side will be terminated automatically. Even if the app is force-closed, the `tokio` async runtime will be properly dropped. +When the Flutter app is closed, the entire `tokio` async runtime on the Rust side doesn't get dropped by default. -When using Rinf, the lifetime of the `tokio` runtime follows that of the Dart runtime. This behavior is different from typical `tokio` executables where its async runtime lives throughout the async `main()` function of Rust. +In some cases, you might need to drop all Rust resources properly before closing the app. This could include instances of structs that implement the `Drop` trait, which have roles like saving files or disposing of resources. -In some cases, you might need to run some finalization code in Rust before the app closes. This might involve saving files or disposing of resources. To achieve this, you can use Flutter's `AppLifecycleListener` to run something or to get user confirmation before closing the Flutter app. +To achieve this, you can utilize Flutter's `AppLifecycleListener` to call the `finalizeRust` function before closing the Flutter app. ```dart title="lib/main.dart" import 'dart:ui'; import 'package:flutter/material.dart'; +import 'package:rinf/rinf.dart'; class MyApp extends StatefulWidget { const MyApp({super.key}); - @override State createState() => _MyAppState(); } class _MyAppState extends State { - final _appLifecycleListener = AppLifecycleListener( - onExitRequested: () async { - // Do something here before the app is exited. - return AppExitResponse.exit; - }, - ); + late final AppLifecycleListener _listener; + + @override + void initState() { + super.initState(); + _listener = AppLifecycleListener( + onExitRequested: () async { + finalizeRust(); // Shut down the `tokio` Rust runtime. + return AppExitResponse.exit; + }, + ); + } @override void dispose() { - _appLifecycleListener.dispose(); + _listener.dispose(); super.dispose(); } @override Widget build(BuildContext context) { - return MaterialApp( - title: 'Some App', - home: MyHomePage(), - ); + // Return a widget. } } ``` -It's worth noting that `AppLifecycleListener` or `dispose` cannot always be relied upon for app closings. Below is a text snippet quoted from the official [Flutter docs](https://api.flutter.dev/flutter/widgets/State/dispose.html): +It's worth noting that `AppLifecycleListener` cannot always be relied upon for app closings. Below is a text snippet quoted from the official [Flutter docs](https://api.flutter.dev/flutter/widgets/State/dispose.html): > There is no way to predict when application shutdown will happen. For example, a user's battery could catch fire, or the user could drop the device into a swimming pool, or the operating system could unilaterally terminate the application process due to memory pressure. Applications are responsible for ensuring they behave well even in the face of rapid, unscheduled termination. diff --git a/flutter_ffi_plugin/CHANGELOG.md b/flutter_ffi_plugin/CHANGELOG.md index 400fe3dc..7e56c0d0 100644 --- a/flutter_ffi_plugin/CHANGELOG.md +++ b/flutter_ffi_plugin/CHANGELOG.md @@ -1,3 +1,10 @@ +## 6.13.0 + +- The `get_dart_signal_receiver` function now returns `Result`. You need to use `unwrap` or `?` to retrieve Dart signal receivers from the generated message structs. +- You now need to manually provide the generated `assignRustSignal` to the `initializeRust` function, which can be imported using `import 'package:rinf/rinf.dart';` in Dart. +- The tokio runtime will now default to being single-threaded. To use a multi-threaded tokio runtime, enable the new `rt-multi-thread` crate feature. +- By default, backtraces will be hidden in the CLI, with only the error message being printed. To display the Rust backtrace, enable the new `backtrace` crate feature. + ## 6.12.1 - Fixed linefeed problem in published files. diff --git a/flutter_ffi_plugin/bin/src/helpers.dart b/flutter_ffi_plugin/bin/src/helpers.dart index a52d67ec..30463c80 100644 --- a/flutter_ffi_plugin/bin/src/helpers.dart +++ b/flutter_ffi_plugin/bin/src/helpers.dart @@ -155,18 +155,22 @@ please refer to Rinf's [documentation](https://rinf.cunarist.com). ); lines.insert( lastImportIndex + 1, + "import 'package:rinf/rinf.dart';", + ); + lines.insert( + lastImportIndex + 2, "import './messages/generated.dart';", ); mainText = lines.join("\n"); } - if (!mainText.contains('initializeRust()')) { + if (!mainText.contains('initializeRust(assignRustSignal)')) { mainText = mainText.replaceFirst( 'main() {', 'main() async {', ); mainText = mainText.replaceFirst( 'main() async {', - 'main() async { await initializeRust();', + 'main() async { await initializeRust(assignRustSignal);', ); } await mainFile.writeAsString(mainText); diff --git a/flutter_ffi_plugin/bin/src/message.dart b/flutter_ffi_plugin/bin/src/message.dart index 7854f517..1f070a9a 100644 --- a/flutter_ffi_plugin/bin/src/message.dart +++ b/flutter_ffi_plugin/bin/src/message.dart @@ -61,7 +61,7 @@ Future generateMessageCode({ final lines = await protoFile.readAsLines(); List outputLines = []; for (var line in lines) { - final packagePattern = r'^package\s+[a-zA-Z_][a-zA-Z0-9_]*\s*[^=];$'; + final packagePattern = r'^package\s+[a-zA-Z_][a-zA-Z0-9_\.]*\s*[^=];$'; if (RegExp(packagePattern).hasMatch(line.trim())) { continue; } else if (line.trim().startsWith("syntax")) { @@ -430,16 +430,16 @@ use std::sync::OnceLock; use tokio::sync::mpsc::unbounded_channel; type Handler = dyn Fn(&[u8], &[u8]) -> Result<(), RinfError> + Send + Sync; -type SignalHandlers = HashMap>; -static SIGNAL_HANDLERS: OnceLock = OnceLock::new(); +type DartSignalHandlers = HashMap>; +static DART_SIGNAL_HANDLERS: OnceLock = OnceLock::new(); -pub fn handle_dart_signal( +pub fn assign_dart_signal( message_id: i32, message_bytes: &[u8], binary: &[u8] ) -> Result<(), RinfError> { - let hash_map = SIGNAL_HANDLERS.get_or_init(|| { - let mut new_hash_map: SignalHandlers = HashMap::new(); + let hash_map = DART_SIGNAL_HANDLERS.get_or_init(|| { + let mut new_hash_map: DartSignalHandlers = HashMap::new(); '''; for (final entry in markedMessagesAll.entries) { final subpath = entry.key; @@ -522,13 +522,7 @@ new_hash_map.insert( import 'dart:typed_data'; import 'package:rinf/rinf.dart'; -Future initializeRust({String? compiledLibPath}) async { - setCompiledLibPath(compiledLibPath); - await prepareInterface(handleRustSignal); - startRustLogic(); -} - -final signalHandlers = { +final rustSignalHandlers = { '''; for (final entry in markedMessagesAll.entries) { final subpath = entry.key; @@ -568,8 +562,8 @@ ${markedMessage.id}: (Uint8List messageBytes, Uint8List binary) { dartReceiveScript += ''' }; -void handleRustSignal(int messageId, Uint8List messageBytes, Uint8List binary) { - signalHandlers[messageId]!(messageBytes, binary); +void assignRustSignal(int messageId, Uint8List messageBytes, Uint8List binary) { + rustSignalHandlers[messageId]!(messageBytes, binary); } '''; await File.fromUri(dartOutputPath.join('generated.dart')) diff --git a/flutter_ffi_plugin/example/lib/main.dart b/flutter_ffi_plugin/example/lib/main.dart index 7a8fe6bd..93048d80 100755 --- a/flutter_ffi_plugin/example/lib/main.dart +++ b/flutter_ffi_plugin/example/lib/main.dart @@ -1,15 +1,46 @@ +import 'dart:ui'; import 'package:flutter/material.dart'; -import 'package:example_app/messages/generated.dart'; -import 'package:example_app/messages/counter_number.pb.dart'; -import 'package:example_app/messages/fractal_art.pb.dart'; +import 'package:rinf/rinf.dart'; +import './messages/generated.dart'; +import './messages/counter_number.pb.dart'; +import './messages/fractal_art.pb.dart'; void main() async { - // Wait for Rust initialization to be completed first. - await initializeRust(); + await initializeRust(assignRustSignal); runApp(MyApp()); } -class MyApp extends StatelessWidget { +class MyApp extends StatefulWidget { + const MyApp({super.key}); + @override + State createState() => _MyAppState(); +} + +class _MyAppState extends State { + /// This `AppLifecycleListener` is responsible for the + /// graceful shutdown of the async runtime in Rust. + /// If you don't care about + /// properly dropping Rust objects before shutdown, + /// creating this listener is not necessary. + late final AppLifecycleListener _listener; + + @override + void initState() { + super.initState(); + _listener = AppLifecycleListener( + onExitRequested: () async { + finalizeRust(); // This line shuts down the async Rust runtime. + return AppExitResponse.exit; + }, + ); + } + + @override + void dispose() { + _listener.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return MaterialApp( @@ -30,71 +61,11 @@ class MyHomePage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - // `StreamBuilder` listens to a stream - // and rebuilds the widget accordingly. - StreamBuilder( - stream: SampleFractal.rustSignalStream, - builder: (context, snapshot) { - final rustSignal = snapshot.data; - if (rustSignal == null) { - return Container( - margin: const EdgeInsets.all(20), - width: 256, - height: 256, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(24.0), - color: Colors.black, - ), - ); - } - final imageData = rustSignal.binary; - return Container( - margin: const EdgeInsets.all(20), - width: 256, - height: 256, - child: ClipRRect( - borderRadius: BorderRadius.circular(24.0), - child: FittedBox( - fit: BoxFit.contain, - child: Image.memory( - imageData, - width: 256, - height: 256, - gaplessPlayback: true, - ), - ), - ), - ); - }), - StreamBuilder( - // This stream is generated from a marked Protobuf message. - stream: SampleNumberOutput.rustSignalStream, - builder: (context, snapshot) { - final rustSignal = snapshot.data; - // If the app has just started and widget is built - // without receiving a Rust signal, - // the snapshot data will be null. - // It's when the widget is being built for the first time. - if (rustSignal == null) { - // Return the initial widget if the snapshot data is null. - return Text('Initial value 0'); - } - final sampleNumberOutput = rustSignal.message; - final currentNumber = sampleNumberOutput.currentNumber; - return Text('Current value is $currentNumber'); - }, - ), - ], - ), - ), - // This is a button that calls the generated function. + body: Center(child: MyColumn()), floatingActionButton: FloatingActionButton( onPressed: () async { - // The method is generated from a marked Protobuf message. + // The `sendSignalToRust` method is generated + // from a marked Protobuf message. SampleNumberInput( letter: "HELLO FROM DART!", dummyOne: 25, @@ -111,3 +82,69 @@ class MyHomePage extends StatelessWidget { ); } } + +class MyColumn extends StatelessWidget { + @override + Widget build(BuildContext context) { + final children = [ + // `StreamBuilder` listens to a stream + // and rebuilds the widget accordingly. + StreamBuilder( + stream: SampleFractal.rustSignalStream, + builder: (context, snapshot) { + final rustSignal = snapshot.data; + if (rustSignal == null) { + return Container( + margin: const EdgeInsets.all(20), + width: 256, + height: 256, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(24.0), + color: Colors.black, + ), + ); + } + final imageData = rustSignal.binary; + return Container( + margin: const EdgeInsets.all(20), + width: 256, + height: 256, + child: ClipRRect( + borderRadius: BorderRadius.circular(24.0), + child: FittedBox( + fit: BoxFit.contain, + child: Image.memory( + imageData, + width: 256, + height: 256, + gaplessPlayback: true, + ), + ), + ), + ); + }), + StreamBuilder( + // This stream is generated from a marked Protobuf message. + stream: SampleNumberOutput.rustSignalStream, + builder: (context, snapshot) { + final rustSignal = snapshot.data; + // If the app has just started and widget is built + // without receiving a Rust signal, + // the snapshot data will be null. + // It's when the widget is being built for the first time. + if (rustSignal == null) { + // Return the initial widget if the snapshot data is null. + return Text('Initial value 0'); + } + final sampleNumberOutput = rustSignal.message; + final currentNumber = sampleNumberOutput.currentNumber; + return Text('Current value is $currentNumber'); + }, + ), + ]; + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: children, + ); + } +} diff --git a/flutter_ffi_plugin/example/native/hub/Cargo.toml b/flutter_ffi_plugin/example/native/hub/Cargo.toml index 77b69f15..89f45de5 100755 --- a/flutter_ffi_plugin/example/native/hub/Cargo.toml +++ b/flutter_ffi_plugin/example/native/hub/Cargo.toml @@ -12,7 +12,7 @@ edition = "2021" crate-type = ["lib", "cdylib", "staticlib"] [dependencies] -rinf = "6.12.1" +rinf = "6.13.0" prost = "0.12.6" tokio = { version = "1", features = ["rt", "sync", "macros", "time"] } tokio_with_wasm = { version = "0.6.1", features = [ diff --git a/flutter_ffi_plugin/lib/rinf.dart b/flutter_ffi_plugin/lib/rinf.dart index 41ae3984..1926afb5 100755 --- a/flutter_ffi_plugin/lib/rinf.dart +++ b/flutter_ffi_plugin/lib/rinf.dart @@ -6,27 +6,27 @@ import 'src/exports.dart'; export 'src/interface.dart' show RustSignal; -/// Sets the exact file path of the dynamic library -/// compiled from the `hub` crate. -/// On the web, this function sets the path to the JavaScript module -/// that needs to be loaded. -/// This function might not be necessary for major platforms -/// but can be useful when the app runs on embedded devices. -void setCompiledLibPath(String? path) { - setCompiledLibPathReal(path); -} - -/// Prepares the native interface -/// needed to communicate with Rust. -Future prepareInterface(HandleRustSignal handleRustSignal) async { - await prepareInterfaceReal(handleRustSignal); -} - /// Starts the `main` function in Rust. -void startRustLogic() async { +Future initializeRust( + AssignRustSignal assignRustSignal, { + String? compiledLibPath, +}) async { + if (compiledLibPath != null) { + setCompiledLibPathReal(compiledLibPath); + } + await prepareInterfaceReal(assignRustSignal); startRustLogicReal(); } +/// Terminates all Rust tasks by dropping the async runtime. +/// Calling this function before closing the Flutter app +/// can prevent potential resource leaks. +/// Please note that on the web, this function does not have any effect, +/// as tasks are managed by the JavaScript runtime, not Rust. +void finalizeRust() async { + stopRustLogicReal(); +} + /// Sends a signal to Rust. void sendDartSignal( int messageId, diff --git a/flutter_ffi_plugin/lib/src/interface.dart b/flutter_ffi_plugin/lib/src/interface.dart index a3bc1c8a..888891d3 100644 --- a/flutter_ffi_plugin/lib/src/interface.dart +++ b/flutter_ffi_plugin/lib/src/interface.dart @@ -3,7 +3,7 @@ import 'dart:typed_data'; /// This type represents a function /// that can accept raw signal data from Rust /// and handle it accordingly. -typedef HandleRustSignal = void Function(int, Uint8List, Uint8List); +typedef AssignRustSignal = void Function(int, Uint8List, Uint8List); /// This contains a message from Rust. /// Optionally, a custom binary called `binary` can also be included. diff --git a/flutter_ffi_plugin/lib/src/interface_os.dart b/flutter_ffi_plugin/lib/src/interface_os.dart index 81610d16..f08dae7f 100644 --- a/flutter_ffi_plugin/lib/src/interface_os.dart +++ b/flutter_ffi_plugin/lib/src/interface_os.dart @@ -7,12 +7,14 @@ import 'dart:isolate'; import 'interface.dart'; import 'dart:convert'; -void setCompiledLibPathReal(String? path) { +/// Sets the exact file path of the dynamic library +/// compiled from the `hub` crate. +void setCompiledLibPathReal(String path) { setDynamicLibPath(path); } Future prepareInterfaceReal( - HandleRustSignal handleRustSignal, + AssignRustSignal assignRustSignal, ) async { /// This should be called once at startup /// to enable `allo_isolate` to send data from the Rust side. @@ -49,7 +51,7 @@ Future prepareInterfaceReal( // Converting is needed on the Dart side. messageBytes = Uint8List(0); } - handleRustSignal(messageId, messageBytes, binary); + assignRustSignal(messageId, messageBytes, binary); }); // Make Rust prepare its isolate to send data to Dart. @@ -64,6 +66,14 @@ void startRustLogicReal() { rustFunction(); } +void stopRustLogicReal() { + final rustFunction = + rustLibrary.lookupFunction( + 'stop_rust_logic_extern', + ); + rustFunction(); +} + /// Sends bytes to Rust. Future sendDartSignalReal( int messageId, diff --git a/flutter_ffi_plugin/lib/src/interface_web.dart b/flutter_ffi_plugin/lib/src/interface_web.dart index 4e1fb016..cac787e0 100644 --- a/flutter_ffi_plugin/lib/src/interface_web.dart +++ b/flutter_ffi_plugin/lib/src/interface_web.dart @@ -7,12 +7,14 @@ import 'interface.dart'; import 'dart:async'; import 'dart:convert'; -void setCompiledLibPathReal(String? path) { +/// Sets the path to the JavaScript module +/// that needs to be loaded. +void setCompiledLibPathReal(String path) { setJsLibPath(path); } Future prepareInterfaceReal( - HandleRustSignal handleRustSignal, + AssignRustSignal assignRustSignal, ) async { await loadJsFile(); @@ -29,7 +31,7 @@ Future prepareInterfaceReal( print(rustReport); return; } - handleRustSignal(messageId, messageBytes, binary); + assignRustSignal(messageId, messageBytes, binary); }; } @@ -41,6 +43,10 @@ void startRustLogicReal() { jsObject.callMethod('start_rust_logic_extern', []); } +void stopRustLogicReal() { + // Dummy function to match the structure of native platforms. +} + void sendDartSignalReal( int messageId, Uint8List messageBytes, diff --git a/flutter_ffi_plugin/lib/src/load_os.dart b/flutter_ffi_plugin/lib/src/load_os.dart index 2b118966..c5c825ed 100644 --- a/flutter_ffi_plugin/lib/src/load_os.dart +++ b/flutter_ffi_plugin/lib/src/load_os.dart @@ -4,7 +4,7 @@ import 'dart:ffi'; String? dynamicLibPath; final rustLibrary = loadRustLibrary(); -void setDynamicLibPath(String? path) { +void setDynamicLibPath(String path) { dynamicLibPath = path; } diff --git a/flutter_ffi_plugin/lib/src/load_web.dart b/flutter_ffi_plugin/lib/src/load_web.dart index a4861d5b..a339834a 100644 --- a/flutter_ffi_plugin/lib/src/load_web.dart +++ b/flutter_ffi_plugin/lib/src/load_web.dart @@ -11,7 +11,7 @@ String? jsLibPath; // as a global JavaScript variable. final wasAlreadyLoaded = js.context.hasProperty("rinf"); -void setJsLibPath(String? path) { +void setJsLibPath(String path) { jsLibPath = path; } diff --git a/flutter_ffi_plugin/pubspec.yaml b/flutter_ffi_plugin/pubspec.yaml index 94b9c5ba..1310f1be 100644 --- a/flutter_ffi_plugin/pubspec.yaml +++ b/flutter_ffi_plugin/pubspec.yaml @@ -1,6 +1,6 @@ name: rinf description: Rust for native business logic, Flutter for flexible and beautiful GUI -version: 6.12.1 +version: 6.13.0 repository: https://github.com/cunarist/rinf environment: diff --git a/flutter_ffi_plugin/template/native/hub/Cargo.toml b/flutter_ffi_plugin/template/native/hub/Cargo.toml index a739bfc7..b4046c33 100644 --- a/flutter_ffi_plugin/template/native/hub/Cargo.toml +++ b/flutter_ffi_plugin/template/native/hub/Cargo.toml @@ -12,7 +12,7 @@ edition = "2021" crate-type = ["lib", "cdylib", "staticlib"] [dependencies] -rinf = "6.12.1" +rinf = "6.13.0" prost = "0.12.6" tokio = { version = "1", features = ["sync", "rt"] } diff --git a/rust_crate/Cargo.toml b/rust_crate/Cargo.toml index 7e3d4e9b..a4d827ea 100644 --- a/rust_crate/Cargo.toml +++ b/rust_crate/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rinf" -version = "6.12.1" +version = "6.13.0" edition = "2021" license = "MIT" description = "Rust for native business logic, Flutter for flexible and beautiful GUI" @@ -9,7 +9,7 @@ documentation = "https://rinf.cunarist.com" rust-version = "1.70" [features] -multi-worker = ["tokio/rt-multi-thread"] +rt-multi-thread = ["tokio/rt-multi-thread"] show-backtrace = ["backtrace"] bevy = ["dep:bevy_ecs"] diff --git a/rust_crate/src/interface_os.rs b/rust_crate/src/interface_os.rs index dfb07899..7d72a025 100644 --- a/rust_crate/src/interface_os.rs +++ b/rust_crate/src/interface_os.rs @@ -8,6 +8,7 @@ use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex, OnceLock}; use std::task::{Context, Poll, Waker}; use std::thread; +use std::thread::{current, park, Thread}; use tokio::runtime::Builder; static DART_ISOLATE: Mutex> = Mutex::new(None); @@ -30,8 +31,9 @@ pub extern "C" fn prepare_isolate_extern(port: i64) { // and the main thread exits unexpectedly, // the whole async tokio runtime can shut down as well // by receiving a signal via the shutdown channel. -// Without this solution, zombie threads inside the tokio runtime -// might outlive the app. +// Without this solution, +// zombie threads inside the tokio runtime might outlive the app. +// This `ThreadLocal` is intended to be used only on the main thread. type ShutdownSenderLock = OnceLock>>>; static SHUTDOWN_SENDER: ShutdownSenderLock = OnceLock::new(); @@ -59,13 +61,12 @@ where // Prepare the channel that will notify tokio runtime to shutdown // after the main Dart thread has gone. - let (shutdown_sender, shutdown_receiver) = shutdown_channel(); - let shutdown_sender_lock = - SHUTDOWN_SENDER.get_or_init(move || ThreadLocal::new(|| RefCell::new(None))); - shutdown_sender_lock.with(|cell| cell.replace(Some(shutdown_sender))); + let (shutdown_sender, shutdown_receiver, shutdown_reporter) = shutdown_channel(); + let sender_lock = SHUTDOWN_SENDER.get_or_init(move || ThreadLocal::new(|| RefCell::new(None))); + sender_lock.with(|cell| cell.replace(Some(shutdown_sender))); // Build the tokio runtime. - #[cfg(not(feature = "multi-worker"))] + #[cfg(not(feature = "rt-multi-thread"))] { let tokio_runtime = Builder::new_current_thread() .enable_all() @@ -76,9 +77,11 @@ where tokio_runtime.block_on(shutdown_receiver); // Dropping the tokio runtime makes it shut down. drop(tokio_runtime); + // After dropping the runtime, tell the main thread to stop waiting. + drop(shutdown_reporter); }); } - #[cfg(feature = "multi-worker")] + #[cfg(feature = "rt-multi-thread")] { static TOKIO_RUNTIME: Mutex> = Mutex::new(None); let tokio_runtime = Builder::new_multi_thread() @@ -97,6 +100,8 @@ where // Dropping the tokio runtime makes it shut down. drop(runtime); } + // After dropping the runtime, tell the main thread to stop waiting. + drop(shutdown_reporter); } }) }); @@ -115,6 +120,18 @@ where Ok(()) } +#[no_mangle] +pub extern "C" fn stop_rust_logic_extern() { + let sender_lock = SHUTDOWN_SENDER.get_or_init(move || ThreadLocal::new(|| RefCell::new(None))); + let sender_option = sender_lock.with(|cell| cell.take()); + if let Some(shutdown_sender) = sender_option { + // Dropping the sender tells the tokio runtime to stop running. + // Also, it blocks the main thread until + // it gets the report that tokio shutdown is dropped. + drop(shutdown_sender); + } +} + pub fn send_rust_signal_real( message_id: i32, message_bytes: Vec, @@ -154,49 +171,80 @@ pub fn send_rust_signal_real( } struct ShutdownSender { - is_sent: Arc, + should_shutdown: Arc, + did_shutdown: Arc, waker: Arc>>, } impl Drop for ShutdownSender { fn drop(&mut self) { - self.is_sent.store(true, Ordering::SeqCst); + self.should_shutdown.store(true, Ordering::SeqCst); if let Ok(mut guard) = self.waker.lock() { if let Some(waker) = guard.take() { waker.wake(); } } + while !self.did_shutdown.load(Ordering::SeqCst) { + // Dropping the sender is always done on the main thread. + park(); + } } } struct ShutdownReceiver { - is_sent: Arc, + should_shutdown: Arc, waker: Arc>>, } impl Future for ShutdownReceiver { type Output = (); fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - if self.is_sent.load(Ordering::SeqCst) { - Poll::Ready(()) - } else { + if !self.should_shutdown.load(Ordering::SeqCst) { if let Ok(mut guard) = self.waker.lock() { guard.replace(cx.waker().clone()); } Poll::Pending + } else { + Poll::Ready(()) } } } -fn shutdown_channel() -> (ShutdownSender, ShutdownReceiver) { - let is_sent = Arc::new(AtomicBool::new(false)); +type ChannelTuple = (ShutdownSender, ShutdownReceiver, ShutdownReporter); +fn shutdown_channel() -> ChannelTuple { + // This code assumes that + // this function is being called from the main thread. + let main_thread = current(); + + let should_shutdown = Arc::new(AtomicBool::new(false)); + let is_done = Arc::new(AtomicBool::new(false)); let waker = Arc::new(Mutex::new(None)); let sender = ShutdownSender { - is_sent: Arc::clone(&is_sent), - waker: Arc::clone(&waker), + should_shutdown: should_shutdown.clone(), + waker: waker.clone(), + did_shutdown: is_done.clone(), + }; + let receiver = ShutdownReceiver { + should_shutdown, + waker, + }; + let reporter = ShutdownReporter { + is_done, + main_thread, }; - let receiver = ShutdownReceiver { is_sent, waker }; - (sender, receiver) + (sender, receiver, reporter) +} + +struct ShutdownReporter { + is_done: Arc, + main_thread: Thread, +} + +impl Drop for ShutdownReporter { + fn drop(&mut self) { + self.is_done.store(true, Ordering::SeqCst); + self.main_thread.unpark(); + } } diff --git a/rust_crate/src/macros.rs b/rust_crate/src/macros.rs index d6042b23..d9f690e5 100644 --- a/rust_crate/src/macros.rs +++ b/rust_crate/src/macros.rs @@ -35,7 +35,7 @@ macro_rules! write_interface { use std::slice::from_raw_parts; let message_bytes = unsafe { from_raw_parts(message_pointer, message_size) }; let binary = unsafe { from_raw_parts(binary_pointer, binary_size) }; - let result = messages::generated::handle_dart_signal(message_id, message_bytes, binary); + let result = messages::generated::assign_dart_signal(message_id, message_bytes, binary); if let Err(error) = result { rinf::debug_print!("{error}"); } @@ -46,7 +46,7 @@ macro_rules! write_interface { pub fn send_dart_signal_extern(message_id: i32, message_bytes: &[u8], binary: &[u8]) { let message_bytes = message_bytes; let binary = binary; - let result = messages::generated::handle_dart_signal(message_id, message_bytes, binary); + let result = messages::generated::assign_dart_signal(message_id, message_bytes, binary); if let Err(error) = result { rinf::debug_print!("{error}"); }