From 5fd1cbc57c81b40297d93383574eb3cfc9fcb1d5 Mon Sep 17 00:00:00 2001 From: Donghyun Kim Date: Wed, 19 Jun 2024 22:52:58 +0900 Subject: [PATCH 01/12] Manage async runtime with widget --- flutter_ffi_plugin/bin/src/message.dart | 22 ++--- flutter_ffi_plugin/example/lib/main.dart | 8 +- flutter_ffi_plugin/lib/rinf.dart | 54 ++++++++++-- flutter_ffi_plugin/lib/src/interface.dart | 2 +- flutter_ffi_plugin/lib/src/interface_os.dart | 12 ++- flutter_ffi_plugin/lib/src/interface_web.dart | 8 +- rust_crate/src/interface_os.rs | 88 +++++++++++++++---- rust_crate/src/macros.rs | 4 +- 8 files changed, 149 insertions(+), 49 deletions(-) diff --git a/flutter_ffi_plugin/bin/src/message.dart b/flutter_ffi_plugin/bin/src/message.dart index 7854f517..5de1b4c6 100644 --- a/flutter_ffi_plugin/bin/src/message.dart +++ b/flutter_ffi_plugin/bin/src/message.dart @@ -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..b2c593b7 100755 --- a/flutter_ffi_plugin/example/lib/main.dart +++ b/flutter_ffi_plugin/example/lib/main.dart @@ -1,12 +1,14 @@ import 'package:flutter/material.dart'; +import 'package:rinf/rinf.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'; void main() async { - // Wait for Rust initialization to be completed first. - await initializeRust(); - runApp(MyApp()); + runApp(Rusty( + child: MyApp(), + assignRustSignal: assignRustSignal, + )); } class MyApp extends StatelessWidget { diff --git a/flutter_ffi_plugin/lib/rinf.dart b/flutter_ffi_plugin/lib/rinf.dart index 41ae3984..81035930 100755 --- a/flutter_ffi_plugin/lib/rinf.dart +++ b/flutter_ffi_plugin/lib/rinf.dart @@ -2,6 +2,7 @@ library; import 'dart:typed_data'; +import 'package:flutter/material.dart'; import 'src/exports.dart'; export 'src/interface.dart' show RustSignal; @@ -16,15 +17,54 @@ void setCompiledLibPath(String? path) { setCompiledLibPathReal(path); } -/// Prepares the native interface -/// needed to communicate with Rust. -Future prepareInterface(HandleRustSignal handleRustSignal) async { - await prepareInterfaceReal(handleRustSignal); +/// This widget manages the lifecycle of the +/// async runtime on the Rust side. +/// In essense, this widget is responsible of +/// creation and graceful shutdown of the async runtime in Rust. +/// The Rust async runtime will be created +/// when the state of this widget is initialized, +/// and it will be shut down when this widget is disposed. +class Rusty extends StatefulWidget { + final Widget child; + final AssignRustSignal assignRustSignal; + const Rusty({ + required this.child, + required this.assignRustSignal, + }); + + @override + State createState() => _RustyState(); } -/// Starts the `main` function in Rust. -void startRustLogic() async { - startRustLogicReal(); +class _RustyState extends State { + late Future initialLoad; + + @override + void initState() { + super.initState(); + initialLoad = () async { + await prepareInterfaceReal(widget.assignRustSignal); + startRustLogicReal(); + print("START DART (TEMP)"); + }(); + } + + @override + void dispose() { + print("STOP DART (TEMP)"); + stopRustLogicReal(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: initialLoad, + builder: (context, snapshot) { + return widget.child; + }, + ); + } } /// Sends a signal to Rust. 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..5d937a72 100644 --- a/flutter_ffi_plugin/lib/src/interface_os.dart +++ b/flutter_ffi_plugin/lib/src/interface_os.dart @@ -12,7 +12,7 @@ void setCompiledLibPathReal(String? 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 +49,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 +64,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..829d01e6 100644 --- a/flutter_ffi_plugin/lib/src/interface_web.dart +++ b/flutter_ffi_plugin/lib/src/interface_web.dart @@ -12,7 +12,7 @@ void setCompiledLibPathReal(String? path) { } Future prepareInterfaceReal( - HandleRustSignal handleRustSignal, + AssignRustSignal assignRustSignal, ) async { await loadJsFile(); @@ -29,7 +29,7 @@ Future prepareInterfaceReal( print(rustReport); return; } - handleRustSignal(messageId, messageBytes, binary); + assignRustSignal(messageId, messageBytes, binary); }; } @@ -41,6 +41,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/rust_crate/src/interface_os.rs b/rust_crate/src/interface_os.rs index dfb07899..1411b937 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,10 +61,9 @@ 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"))] @@ -76,6 +77,10 @@ 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. + println!("RUNTIME GONE! (TEMP)"); + drop(shutdown_reporter); + println!("ALMOST DONE! (TEMP)"); }); } #[cfg(feature = "multi-worker")] @@ -97,6 +102,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 +122,19 @@ where Ok(()) } +#[no_mangle] +pub extern "C" fn stop_rust_logic_extern() { + println!("NOW STOP (TEMP)"); + 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 +174,81 @@ 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(); + } + println!("EVERYTHING DONE! (TEMP)"); } } 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}"); } From d5daaa41a646711f1c4543bf3e12dacc71446ce4 Mon Sep 17 00:00:00 2001 From: Donghyun Kim Date: Mon, 24 Jun 2024 00:39:06 +0900 Subject: [PATCH 02/12] Allow dot in the proto package name --- flutter_ffi_plugin/bin/src/message.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter_ffi_plugin/bin/src/message.dart b/flutter_ffi_plugin/bin/src/message.dart index 7854f517..1540d67b 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")) { From 84893e75f377e17ffa767f8cdf118f62bd4b3b7b Mon Sep 17 00:00:00 2001 From: Donghyun Kim Date: Sat, 29 Jun 2024 10:21:16 +0900 Subject: [PATCH 03/12] Provide `initializeRust` and `finalizeRust` --- flutter_ffi_plugin/bin/src/helpers.dart | 8 +- flutter_ffi_plugin/example/lib/main.dart | 12 ++- flutter_ffi_plugin/lib/rinf.dart | 75 +++++-------------- flutter_ffi_plugin/lib/src/interface_os.dart | 4 +- flutter_ffi_plugin/lib/src/interface_web.dart | 4 +- flutter_ffi_plugin/lib/src/load_os.dart | 2 +- flutter_ffi_plugin/lib/src/load_web.dart | 2 +- 7 files changed, 37 insertions(+), 70 deletions(-) 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/example/lib/main.dart b/flutter_ffi_plugin/example/lib/main.dart index b2c593b7..acd2e369 100755 --- a/flutter_ffi_plugin/example/lib/main.dart +++ b/flutter_ffi_plugin/example/lib/main.dart @@ -1,14 +1,12 @@ import 'package:flutter/material.dart'; import 'package:rinf/rinf.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 './messages/generated.dart'; +import './messages/counter_number.pb.dart'; +import './messages/fractal_art.pb.dart'; void main() async { - runApp(Rusty( - child: MyApp(), - assignRustSignal: assignRustSignal, - )); + await initializeRust(assignRustSignal); + runApp(MyApp()); } class MyApp extends StatelessWidget { diff --git a/flutter_ffi_plugin/lib/rinf.dart b/flutter_ffi_plugin/lib/rinf.dart index 81035930..2faa647b 100755 --- a/flutter_ffi_plugin/lib/rinf.dart +++ b/flutter_ffi_plugin/lib/rinf.dart @@ -2,69 +2,30 @@ library; import 'dart:typed_data'; -import 'package:flutter/material.dart'; 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); -} - -/// This widget manages the lifecycle of the -/// async runtime on the Rust side. -/// In essense, this widget is responsible of -/// creation and graceful shutdown of the async runtime in Rust. -/// The Rust async runtime will be created -/// when the state of this widget is initialized, -/// and it will be shut down when this widget is disposed. -class Rusty extends StatefulWidget { - final Widget child; - final AssignRustSignal assignRustSignal; - const Rusty({ - required this.child, - required this.assignRustSignal, - }); - - @override - State createState() => _RustyState(); -} - -class _RustyState extends State { - late Future initialLoad; - - @override - void initState() { - super.initState(); - initialLoad = () async { - await prepareInterfaceReal(widget.assignRustSignal); - startRustLogicReal(); - print("START DART (TEMP)"); - }(); - } - - @override - void dispose() { - print("STOP DART (TEMP)"); - stopRustLogicReal(); - super.dispose(); +/// Starts the `main` function in Rust. +Future initializeRust( + AssignRustSignal assignRustSignal, { + String? compiledLibPath, +}) async { + if (compiledLibPath != null) { + setCompiledLibPathReal(compiledLibPath); } + await prepareInterfaceReal(assignRustSignal); + startRustLogicReal(); +} - @override - Widget build(BuildContext context) { - return FutureBuilder( - future: initialLoad, - builder: (context, snapshot) { - return widget.child; - }, - ); - } +/// Terminates all Rust tasks. +/// Calling this function before closing the Flutter app +/// can prevent potential resource leaks that may occur +/// if the Rust side is abruptly terminated. +/// 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. diff --git a/flutter_ffi_plugin/lib/src/interface_os.dart b/flutter_ffi_plugin/lib/src/interface_os.dart index 5d937a72..f08dae7f 100644 --- a/flutter_ffi_plugin/lib/src/interface_os.dart +++ b/flutter_ffi_plugin/lib/src/interface_os.dart @@ -7,7 +7,9 @@ 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); } diff --git a/flutter_ffi_plugin/lib/src/interface_web.dart b/flutter_ffi_plugin/lib/src/interface_web.dart index 829d01e6..cac787e0 100644 --- a/flutter_ffi_plugin/lib/src/interface_web.dart +++ b/flutter_ffi_plugin/lib/src/interface_web.dart @@ -7,7 +7,9 @@ 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); } 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; } From 790b96873a84774d5a669f78f6a47214e83d152a Mon Sep 17 00:00:00 2001 From: Donghyun Kim Date: Sat, 29 Jun 2024 10:42:06 +0900 Subject: [PATCH 04/12] Use `AppLifecycleListener` in the example app --- flutter_ffi_plugin/example/lib/main.dart | 33 +++++++++++++++++++++++- rust_crate/src/interface_os.rs | 4 --- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/flutter_ffi_plugin/example/lib/main.dart b/flutter_ffi_plugin/example/lib/main.dart index acd2e369..87032455 100755 --- a/flutter_ffi_plugin/example/lib/main.dart +++ b/flutter_ffi_plugin/example/lib/main.dart @@ -1,3 +1,4 @@ +import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:rinf/rinf.dart'; import './messages/generated.dart'; @@ -9,7 +10,37 @@ void main() async { 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( diff --git a/rust_crate/src/interface_os.rs b/rust_crate/src/interface_os.rs index 1411b937..58002ccb 100644 --- a/rust_crate/src/interface_os.rs +++ b/rust_crate/src/interface_os.rs @@ -78,9 +78,7 @@ where // Dropping the tokio runtime makes it shut down. drop(tokio_runtime); // After dropping the runtime, tell the main thread to stop waiting. - println!("RUNTIME GONE! (TEMP)"); drop(shutdown_reporter); - println!("ALMOST DONE! (TEMP)"); }); } #[cfg(feature = "multi-worker")] @@ -124,7 +122,6 @@ where #[no_mangle] pub extern "C" fn stop_rust_logic_extern() { - println!("NOW STOP (TEMP)"); 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 { @@ -191,7 +188,6 @@ impl Drop for ShutdownSender { // Dropping the sender is always done on the main thread. park(); } - println!("EVERYTHING DONE! (TEMP)"); } } From fa70b8aa3df4566b40f927be5388067ce6d9d87f Mon Sep 17 00:00:00 2001 From: Donghyun Kim Date: Sat, 29 Jun 2024 10:47:08 +0900 Subject: [PATCH 05/12] Improve example app's readability --- flutter_ffi_plugin/example/lib/main.dart | 131 ++++++++++++----------- 1 file changed, 68 insertions(+), 63 deletions(-) diff --git a/flutter_ffi_plugin/example/lib/main.dart b/flutter_ffi_plugin/example/lib/main.dart index 87032455..771afe4f 100755 --- a/flutter_ffi_plugin/example/lib/main.dart +++ b/flutter_ffi_plugin/example/lib/main.dart @@ -61,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, @@ -142,3 +82,68 @@ class MyHomePage extends StatelessWidget { ); } } + +class MyColumn extends StatelessWidget { + @override + Widget build(BuildContext context) { + return 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'); + }, + ), + ], + ); + } +} From faa822e92e52dd46a3da8164605465791cd6a6f7 Mon Sep 17 00:00:00 2001 From: Donghyun Kim Date: Sat, 29 Jun 2024 10:48:54 +0900 Subject: [PATCH 06/12] Improve a doc comment --- flutter_ffi_plugin/lib/rinf.dart | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/flutter_ffi_plugin/lib/rinf.dart b/flutter_ffi_plugin/lib/rinf.dart index 2faa647b..1926afb5 100755 --- a/flutter_ffi_plugin/lib/rinf.dart +++ b/flutter_ffi_plugin/lib/rinf.dart @@ -18,10 +18,9 @@ Future initializeRust( startRustLogicReal(); } -/// Terminates all Rust tasks. +/// Terminates all Rust tasks by dropping the async runtime. /// Calling this function before closing the Flutter app -/// can prevent potential resource leaks that may occur -/// if the Rust side is abruptly terminated. +/// 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 { From ed25c49b01c679917fe66b0b64123f1d4201cfc0 Mon Sep 17 00:00:00 2001 From: Donghyun Kim Date: Sat, 29 Jun 2024 10:50:28 +0900 Subject: [PATCH 07/12] Improve example app's readability --- flutter_ffi_plugin/example/lib/main.dart | 101 ++++++++++++----------- 1 file changed, 51 insertions(+), 50 deletions(-) diff --git a/flutter_ffi_plugin/example/lib/main.dart b/flutter_ffi_plugin/example/lib/main.dart index 771afe4f..93048d80 100755 --- a/flutter_ffi_plugin/example/lib/main.dart +++ b/flutter_ffi_plugin/example/lib/main.dart @@ -86,64 +86,65 @@ class MyHomePage extends StatelessWidget { class MyColumn extends StatelessWidget { @override Widget build(BuildContext context) { - return 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; + 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, - child: ClipRRect( + decoration: BoxDecoration( borderRadius: BorderRadius.circular(24.0), - child: FittedBox( - fit: BoxFit.contain, - child: Image.memory( - imageData, - width: 256, - height: 256, - gaplessPlayback: true, - ), - ), + color: Colors.black, ), ); - }), - 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'); - }, - ), - ], + 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, ); } } From fa6473dafa21164583211c8698fb1bd93056cd03 Mon Sep 17 00:00:00 2001 From: Donghyun Kim Date: Sat, 29 Jun 2024 11:00:58 +0900 Subject: [PATCH 08/12] Update docs about graceful shutdown --- documentation/docs/graceful-shutdown.md | 39 ++++++++++++++++--------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/documentation/docs/graceful-shutdown.md b/documentation/docs/graceful-shutdown.md index 42bd8447..501e0729 100644 --- a/documentation/docs/graceful-shutdown.md +++ b/documentation/docs/graceful-shutdown.md @@ -1,46 +1,59 @@ # 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(); // Shuts 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', + title: 'Rinf Demo', + theme: ThemeData( + colorScheme: ColorScheme.fromSeed( + seedColor: Colors.blueGrey, + brightness: MediaQuery.platformBrightnessOf(context), + ), + useMaterial3: true, + ), home: MyHomePage(), ); } } ``` -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. From 0bb87f74023a8b27f393b1dc41c9f1bb96b321e5 Mon Sep 17 00:00:00 2001 From: Donghyun Kim Date: Sat, 29 Jun 2024 11:03:20 +0900 Subject: [PATCH 09/12] Simplify code snippet --- documentation/docs/graceful-shutdown.md | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/documentation/docs/graceful-shutdown.md b/documentation/docs/graceful-shutdown.md index 501e0729..33b1bfd4 100644 --- a/documentation/docs/graceful-shutdown.md +++ b/documentation/docs/graceful-shutdown.md @@ -25,7 +25,7 @@ class _MyAppState extends State { super.initState(); _listener = AppLifecycleListener( onExitRequested: () async { - finalizeRust(); // Shuts down the `tokio` Rust runtime. + finalizeRust(); // Shut down the `tokio` Rust runtime. return AppExitResponse.exit; }, ); @@ -39,17 +39,7 @@ class _MyAppState extends State { @override Widget build(BuildContext context) { - return MaterialApp( - title: 'Rinf Demo', - theme: ThemeData( - colorScheme: ColorScheme.fromSeed( - seedColor: Colors.blueGrey, - brightness: MediaQuery.platformBrightnessOf(context), - ), - useMaterial3: true, - ), - home: MyHomePage(), - ); + // Return a widget. } } ``` From eb036abc408324ab22bda6ea28eb3d9fd5847266 Mon Sep 17 00:00:00 2001 From: Donghyun Kim Date: Sat, 29 Jun 2024 11:08:24 +0900 Subject: [PATCH 10/12] Rename a feature to `rt-multi-thread` --- documentation/docs/configuration.md | 2 +- rust_crate/Cargo.toml | 2 +- rust_crate/src/interface_os.rs | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) 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/rust_crate/Cargo.toml b/rust_crate/Cargo.toml index 35285330..8685e2f6 100644 --- a/rust_crate/Cargo.toml +++ b/rust_crate/Cargo.toml @@ -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"] [target.'cfg(not(target_family = "wasm"))'.dependencies] diff --git a/rust_crate/src/interface_os.rs b/rust_crate/src/interface_os.rs index 58002ccb..7d72a025 100644 --- a/rust_crate/src/interface_os.rs +++ b/rust_crate/src/interface_os.rs @@ -66,7 +66,7 @@ where 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() @@ -81,7 +81,7 @@ where 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() From 0a04a6308880bf4af5b332e2c9d55924c2c4df33 Mon Sep 17 00:00:00 2001 From: Donghyun Kim Date: Sat, 29 Jun 2024 11:10:50 +0900 Subject: [PATCH 11/12] Clarify docs --- documentation/docs/frequently-asked-questions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 584dcf4ce3bd446993e0abf3e5c9b028b93c3f22 Mon Sep 17 00:00:00 2001 From: Donghyun Kim Date: Sat, 29 Jun 2024 12:03:06 +0900 Subject: [PATCH 12/12] Version 6.13.0 --- CHANGELOG.md | 7 +++++++ flutter_ffi_plugin/CHANGELOG.md | 7 +++++++ flutter_ffi_plugin/example/native/hub/Cargo.toml | 2 +- flutter_ffi_plugin/pubspec.yaml | 2 +- flutter_ffi_plugin/template/native/hub/Cargo.toml | 2 +- rust_crate/Cargo.toml | 2 +- 6 files changed, 18 insertions(+), 4 deletions(-) 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/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/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/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 8685e2f6..a24716d9 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"