LocalSend Onboarding LabChapter VI — Rust FFI Bridge: Dart-to-Rust Boundary0/6Contents
Chapter VI

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.


Crossing the Dart-to-Rust boundary
util/rust.dart conversionsapp/lib/util/rust.dart
rust/api/http.dart & model.dartapp/lib/rust/api/http.dart
rust/src/api/http.rsapp/rust/src/api/http.rs
Provider/UI code…ovider/network/send_provider.dart
core crate (localsend::http::client)app/rust/src/api/http.rs
Figure I · Click a node to see what it does, where it lives in the code, and its labeled connections — the annotation opens beside it.
walkthrough

Converting a Dart FileDto into the Rust-facing DTO

app/lib/util/rust.dart · lines 5067
lines 50–51

This 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.

line 55

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.

line 56

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.

lines 59–64

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,    );  }}
step 1 of 4
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 923
Figure II · create_client is the entry point providers call (via httpProvider) to build an RsHttpClient: it hands this device's own private key and self-signed certificate to the core's LsHttpClient::new, which is how the client's TLS layer is pinned to LocalSend device identities instead of a public CA.
Figure III · data modelMirrored DTOs: one shape, two languagesapp/rust/src/api/model.rs
{
: {
: {
: ProtocolType (enum)
},
: {
: Option<FileMetadata>
}
}
}
walkthrough

How register() bridges core responses and errors back to Dart

app/rust/src/api/http.rs · lines 2543
lines 25–26

This 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.

lines 33–37

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.

lines 39–42

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,        })    }
step 1 of 3
checkpoint

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)?