Rust FFI Bridge: Dart-to-Rust Boundary
This module covers the flutter_rust_bridge (FRB) boundary that every LocalSend transfer crosses: hand-written Rust in app/rust/src/api/http.rs and model.rs wrap the core crate's TLS-pinned HTTP client and its DTOs, FRB codegen turns those into the generated Dart files app/lib/rust/api/http.dart and model.dart, and app/lib/util/rust.dart supplies the hand-written extension methods that convert the app's own Dart models into the generated rust_model DTOs before a call crosses into Rust. Learners should come away able to trace a send/receive call from a provider, through toRust()/toDevice() conversions, across the FFI call, into the Rust wrapper, and down into the core crate — including how errors and TLS-pinned identity flow back the other way.
Converting a Dart FileDto into the Rust-facing DTO
app/lib/util/rust.dart · lines 50–67This extension bolts a toRust() conversion onto the app's own FileDto model, turning it into the rust_model.FileDto that the generated bridge code expects — every DTO that crosses the boundary needs one of these.
File size is boxed as a BigInt because the Rust side stores it as a u64, which can exceed what a Dart int can safely represent on some platforms/backends.
MIME type is derived on the Dart side (via the mime package) with a generic octet-stream fallback, since the Rust core DTO just stores a string and doesn't do its own MIME sniffing.
Nullable metadata is only constructed when present, and DateTime values are pre-formatted into UTC ISO8601 strings on the Dart side, since the wire DTO carries timestamps as plain strings rather than native DateTime objects.
extension FileDtoExt on FileDto { rust_model.FileDto toRust() { return rust_model.FileDto( id: id, fileName: fileName, size: BigInt.from(size), fileType: lookupMimeType(fileName) ?? 'application/octet-stream', sha256: hash, preview: preview, metadata: metadata != null ? rust_model.FileMetadata( modified: metadata!.lastModified?.toUtc().toIso8601String(), accessed: metadata!.lastAccessed?.toUtc().toIso8601String(), ) : null, ); }}
pub struct RsHttpClient { inner: localsend::http::client::LsHttpClient,} #[frb(sync)]pub fn create_client( private_key: String, cert: String, version: LsHttpClientVersion,) -> Result<RsHttpClient, RsHttpClientError> { let inner = localsend::http::client::LsHttpClient::new(&private_key, &cert, version) .map_err(RsHttpClientError::from)?; Ok(RsHttpClient { inner })}
app/rust/src/api/http.rs · lines 9–23app/rust/src/api/model.rsHow register() bridges core responses and errors back to Dart
app/rust/src/api/http.rs · lines 25–43This impl block is the FFI façade around the core LsHttpClient — register is one of four async methods (register/prepare_upload/upload/cancel) that Dart's generated RsHttpClient interface exposes.
The call is delegated straight to the core client, and any ClientError it raises is translated via .map_err(RsHttpClientError::from) into the FFI-safe RsHttpClientError enum — this is the error-boundary translation every method in this file performs.
The core's response is repackaged into an FFI-specific ResultWithPublicKeyRegisterResponseDto before crossing back to Dart, decoupling the public API's shape from the core crate's internal response type.
impl RsHttpClient { pub async fn register( &self, protocol: ProtocolType, ip: &str, port: u16, payload: RegisterDto, ) -> Result<ResultWithPublicKeyRegisterResponseDto, RsHttpClientError> { let response = self .inner .register(protocol, ip, port, payload) .await .map_err(RsHttpClientError::from)?; Ok(ResultWithPublicKeyRegisterResponseDto { public_key: response.public_key, body: response.body, }) }
Checkpoint: crossing the Dart-to-Rust boundary
Answered in place — nothing is graded, everything is explained. 0 / 3 passed
Where is the FileDto struct that both the Rust core and the generated Dart class ultimately agree on actually defined?
In rust.dart's FileDtoExt.toRust(), why is `size` wrapped as `BigInt.from(size)` instead of passed as a plain Dart int?
How does a status-code failure from the core HTTP client end up as something send_provider.dart can catch and act on (e.g. showing a PIN dialog)?