Skip to content

Commit

Permalink
Merge pull request #390 from cunarist/rusty-widget
Browse files Browse the repository at this point in the history
Provide `finalizeRust` to shut down the tokio runtime
  • Loading branch information
temeddix authored Jun 29, 2024
2 parents 5fcce2c + 0a04a63 commit 56e044d
Show file tree
Hide file tree
Showing 15 changed files with 254 additions and 152 deletions.
2 changes: 1 addition & 1 deletion documentation/docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
2 changes: 1 addition & 1 deletion documentation/docs/frequently-asked-questions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
35 changes: 19 additions & 16 deletions documentation/docs/graceful-shutdown.md
Original file line number Diff line number Diff line change
@@ -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<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
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.
8 changes: 6 additions & 2 deletions flutter_ffi_plugin/bin/src/helpers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
22 changes: 8 additions & 14 deletions flutter_ffi_plugin/bin/src/message.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<i32, Box<Handler>>;
static SIGNAL_HANDLERS: OnceLock<SignalHandlers> = OnceLock::new();
type DartSignalHandlers = HashMap<i32, Box<Handler>>;
static DART_SIGNAL_HANDLERS: OnceLock<DartSignalHandlers> = 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;
Expand Down Expand Up @@ -522,13 +522,7 @@ new_hash_map.insert(
import 'dart:typed_data';
import 'package:rinf/rinf.dart';
Future<void> initializeRust({String? compiledLibPath}) async {
setCompiledLibPath(compiledLibPath);
await prepareInterface(handleRustSignal);
startRustLogic();
}
final signalHandlers = <int, void Function(Uint8List, Uint8List)>{
final rustSignalHandlers = <int, void Function(Uint8List, Uint8List)>{
''';
for (final entry in markedMessagesAll.entries) {
final subpath = entry.key;
Expand Down Expand Up @@ -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'))
Expand Down
175 changes: 106 additions & 69 deletions flutter_ffi_plugin/example/lib/main.dart
Original file line number Diff line number Diff line change
@@ -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<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
/// 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(
Expand All @@ -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,
Expand All @@ -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,
);
}
}
34 changes: 17 additions & 17 deletions flutter_ffi_plugin/lib/rinf.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> prepareInterface(HandleRustSignal handleRustSignal) async {
await prepareInterfaceReal(handleRustSignal);
}

/// Starts the `main` function in Rust.
void startRustLogic() async {
Future<void> 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,
Expand Down
Loading

0 comments on commit 56e044d

Please sign in to comment.