Skip to content

Commit

Permalink
Merge branch 'cunarist:main' into bevy_feature
Browse files Browse the repository at this point in the history
  • Loading branch information
Deep-co-de authored Jun 30, 2024
2 parents 79e1633 + 584dcf4 commit 78ff16b
Show file tree
Hide file tree
Showing 20 changed files with 273 additions and 157 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
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.
7 changes: 7 additions & 0 deletions flutter_ffi_plugin/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
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
24 changes: 9 additions & 15 deletions flutter_ffi_plugin/bin/src/message.dart
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ Future<void> generateMessageCode({
final lines = await protoFile.readAsLines();
List<String> 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")) {
Expand Down 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,
);
}
}
2 changes: 1 addition & 1 deletion flutter_ffi_plugin/example/native/hub/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down
Loading

0 comments on commit 78ff16b

Please sign in to comment.