Closed aran closed 1 week ago
Firstly, I guess it is related to https://github.com/fzyzcjy/flutter_rust_bridge/issues/1883.
One hypothesis is that one of the levels of async might be acquiring the FRB-generated RwLock then yielding while still holding it.
I think so.
Another theory is that in the Rust thread pool, multiple Dart-level Futures could be running simultaneously, and if there are both readers and writers of some variable that was &mut and &, they can collide.
Yes that's also a possibility.
I need to think a bit more about this. The single-thread proposal in #1883 may not work in this specific issue, because we are using async
here. If non-async, single-thread is enough to ensure nobody can acquire a lock in-use. But in async, there can easily be some function holding the lock and yielding (e.g. a network request for 1 second).
Do you have any brainstorms about solving this?
Linking https://github.com/fzyzcjy/flutter_rust_bridge/issues/1910#issuecomment-2093883889 since it's closely related.
With the caveat that I'm about 48hrs into learning about this, at a high level my brainstorm would be that a developer needs to choose a concurrency strategy for each part of their data and stick to it holistically across a whole application for that data. Several different strategies could work but they have different constraints and tradeoffs. FRB could in theory support more than one strategy, but each project would have to stick to a single strategy for each piece of data. Here's a few strategies that should work:
An unwrapped data strategy:
&mut
and &
freely#[frb(sync)]
and async functions freelyFRB must be changed to no longer use RwLock
for opaque data, and to allow main-thread non-sync
functions.
A generated lock strategy:
RwLock
/Mutex
&mut
and &
freely. frb(sync)
try_lock
and there is counterintuitive behavior relative to rust since &mut
and &
no longer have safety enforced by the compilerlock()
not try_lock()
, in which case there's different counterintuitive behavior since the semantics of &mut
and &
are changed into runtime lock operations. Deadlocks become possible that would be impossible in Rust only code. A hand-coded synchronization strategy:
RwLock
/Mutex
themselves&mut
or &
, only owned data in function callsfrb(sync)
Describe the bug
It's hard to write correct code with
&mut
that mutates data that is stored in the Rust layer.Steps to reproduce
This repro causes #1910 but it also creates PanicExceptions: https://github.com/aran/frb-exc-repro/blob/mut/lib/main.dart
Logs
Expected behavior
No response
Generated binding code
No response
OS
No response
Version of
flutter_rust_bridge_codegen
No response
Flutter info
No response
Version of
clang++
No response
Additional context
One hypothesis is that one of the levels of async might be acquiring the FRB-generated RwLock then yielding while still holding it. This allows runtime errors. Another theory is that in the Rust thread pool, multiple Dart-level Futures could be running simultaneously, and if there are both readers and writers of some variable that was
&mut
and&
, they can collide.The programming model of
&mut
and&
is often going to be a leaky abstraction w.r.t. theRwLock
model.