269 lines
12 KiB
Markdown
269 lines
12 KiB
Markdown
# Rust API and C# migration guide
|
|
|
|
`MetaCrate` is a native Rust implementation of the API and behavior cataloged
|
|
from `LibreMetaverse`. The compatibility reference is the
|
|
[pinned C# source](https://github.com/cinderblocks/libremetaverse/tree/2aa70bb68513b39795da5d13c88f31b86e85a3ba);
|
|
it is never loaded, invoked, or shipped. Exact type/member mappings live in
|
|
[`RUST-TYPES.tsv`](../api/RUST-TYPES.tsv) and
|
|
[`RUST-MAPPING.tsv`](../api/RUST-MAPPING.tsv). Search those files by C#
|
|
documentation ID when a familiar member has a non-obvious Rust name.
|
|
|
|
## Choosing crates and features
|
|
|
|
Most applications start with `libremetaverse` and add narrower crates only when
|
|
they use those APIs directly.
|
|
|
|
| Crate | Choose it for | Features or system boundary |
|
|
| --- | --- | --- |
|
|
| `libremetaverse` | Grid client, login, network, agents, inventory, assets, appearance, world, and social APIs | Default pure-Rust BC6H/BC7; optional `jpeg2000`, `rust-j2k`, and `vorbis` |
|
|
| `libremetaverse-types` | UUIDs, vectors, matrices, colors, cancellation, compatibility collections, and boundary types | Pure Rust |
|
|
| `libremetaverse-structured-data` | LLSD/OSD XML, JSON, binary, and notation | Pure Rust |
|
|
| `libremetaverse-imaging` | Managed images, TGA/DDS, codec traits | Optional `jpeg2000` or pure-Rust `rust-j2k` |
|
|
| `libremetaverse-imaging-skia` | Common raster formats through Skia | Optional `skia`; native Skia build/cache |
|
|
| `libremetaverse-prim-mesher` | Legacy prim and sculpt geometry | Pure Rust |
|
|
| `libremetaverse-rendering-simple` | Deterministic reference geometry | Pure Rust |
|
|
| `libremetaverse-rendering-mesh-foundry` | Prim, terrain, sculpt, and mesh-asset rendering | Pure Rust |
|
|
| `libremetaverse-lsl-tools` | LSL lexing, parsing, diagnostics, and generation | Pure Rust |
|
|
| `libremetaverse-rlv` | RLV commands, restrictions, locks, camera, and inventory policy | Pure Rust |
|
|
| `libremetaverse-utilities` | Compatible utility helpers | Pure Rust |
|
|
| `libremetaverse-voice-vivox` | Vivox XML control protocol | External Vivox service is explicit and never spawned |
|
|
| `libremetaverse-voice-webrtc` | Native ICE/DTLS/SRTP/SCTP and Opus voice | System libopus; optional `real-audio` uses CPAL |
|
|
| `libremetaverse-openjpeg` | Audited `OpenJPEG` adapter | System `OpenJPEG` 2.5.4 or newer |
|
|
| `libremetaverse-opus` | Audited Opus encoder/decoder adapter | System libopus |
|
|
|
|
Features are additive. Keep defaults unless you need a codec, and enable one
|
|
native adapter at a time while diagnosing installation problems. The
|
|
[release CI matrix](release-ci-matrix.md) records every validated combination.
|
|
|
|
## Naming and overload migration
|
|
|
|
C# `PascalCase` types remain recognizable while methods and properties use
|
|
Rust `snake_case`. A property getter becomes `name()` and its setter becomes
|
|
`set_name(value)`. Events become `subscribe_*` methods returning an owned
|
|
subscription. Rust has no overloads, so the simplest form keeps the base name
|
|
and additional forms receive parameter-derived suffixes. The mapping ledger is
|
|
authoritative; do not guess a long suffix.
|
|
|
|
```rust
|
|
use libremetaverse::InventoryItem;
|
|
use libremetaverse_types::UUID;
|
|
|
|
let id = UUID::new_with_u_int64(42)?;
|
|
let mut item = InventoryItem::new_with_uuid(id)?;
|
|
item.base.set_name("Migrated item".into());
|
|
assert_eq!(item.base.uuid(), id);
|
|
assert_eq!(item.base.name(), "Migrated item");
|
|
# Ok::<(), libremetaverse::Error>(())
|
|
```
|
|
|
|
C# `null` usually maps to `Option<T>`, `ref`/`out` may become a return value or
|
|
an explicit mutable reference, and interface objects become `dyn Trait` behind
|
|
`Arc` or `Box` according to the ledger's `ownership` column.
|
|
|
|
## Ownership and disposal
|
|
|
|
Managers cloned from a `GridClient` share native state. `GridClient` owns its
|
|
cancellation root and cached services; event guards own registrations; session
|
|
objects own tasks and sockets. Explicit shutdown is recommended because it can
|
|
report errors, while `Drop` remains the final idempotent safety net.
|
|
|
|
```rust
|
|
use libremetaverse::{ClientLifecycleState, GridClient};
|
|
|
|
let client = GridClient::new()?;
|
|
assert_eq!(client.lifecycle_state(), ClientLifecycleState::Active);
|
|
client.dispose_with_method()?;
|
|
client.dispose_with_method()?; // idempotent
|
|
assert_eq!(client.lifecycle_state(), ClientLifecycleState::Disposed);
|
|
# Ok::<(), libremetaverse::Error>(())
|
|
```
|
|
|
|
The runnable version is
|
|
[`offline_client.rs`](../crates/libremetaverse/examples/offline_client.rs).
|
|
Never hold a manager lock while calling user code or awaiting I/O. Services
|
|
registered through `GridClient::builder()` must join every owned task before
|
|
their `shutdown` method returns.
|
|
|
|
## Async work and cancellation
|
|
|
|
Async APIs borrow no hidden runtime. Call them from the application executor and
|
|
pass `CancellationToken` explicitly where the C# API accepted one. Cloning a
|
|
token is cheap and observes the same cancellation source. Cancellation is a
|
|
typed `Error::Cancelled`, not a successful empty response.
|
|
|
|
```rust
|
|
use libremetaverse_types::compat::CancellationTokenSource;
|
|
|
|
# #[tokio::main(flavor = "current_thread")]
|
|
# async fn main() -> Result<(), libremetaverse::Error> {
|
|
let source = CancellationTokenSource::new();
|
|
let token = source.token();
|
|
source.cancel();
|
|
token.cancelled().await;
|
|
assert!(matches!(
|
|
token.throw_if_cancellation_requested(),
|
|
Err(libremetaverse::Error::Cancelled)
|
|
));
|
|
# Ok(())
|
|
# }
|
|
```
|
|
|
|
The same code is available as
|
|
[`cancellation.rs`](../crates/libremetaverse/examples/cancellation.rs).
|
|
Timeouts belong at the caller or documented operation boundary; cancellation
|
|
must still drain and join the underlying resource owner.
|
|
|
|
## Errors
|
|
|
|
Fallible compatibility APIs return the shared `libremetaverse::Error`; native
|
|
composition APIs may expose a narrower error such as `ClientCoreError` or
|
|
`WebRtcError`. Match variants for control flow and use display text only for
|
|
operators. Errors and `Debug` output redact credentials, capability URLs, and
|
|
session secrets.
|
|
|
|
```rust
|
|
use libremetaverse::http::DownloadRequest;
|
|
use libremetaverse_types::compat::Uri;
|
|
|
|
let result = DownloadRequest::new(Uri("file:///not-http".into()), None, None);
|
|
assert!(matches!(result, Err(libremetaverse::Error::Argument)));
|
|
```
|
|
|
|
Do not translate C# exception swallowing into `unwrap_or_default()`. Preserve
|
|
the mapped error contract and handle cancellation separately from protocol,
|
|
I/O, authentication, and validation failures.
|
|
|
|
## Events and subscriptions
|
|
|
|
An `EventHandler<T>` is an `Arc` callback. Keep the returned `Subscription` for
|
|
exactly as long as notifications are wanted; dropping it unregisters the
|
|
callback. Dispatch clones the handler list and releases internal locks before
|
|
calling user code. Manager event dispatch isolates subscriber panics so one
|
|
consumer cannot stop later consumers.
|
|
|
|
```rust
|
|
use libremetaverse::{GridClient, Inventory, InventoryFolder};
|
|
use libremetaverse_types::UUID;
|
|
use std::sync::Arc;
|
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
|
|
|
let client = Arc::new(GridClient::new()?);
|
|
let inventory = Inventory::new_with_grid_client_uuid(Arc::clone(&client), UUID::zero())?;
|
|
let calls = Arc::new(AtomicUsize::new(0));
|
|
let observed = Arc::clone(&calls);
|
|
let subscription = inventory.subscribe_inventory_object_added(Arc::new(move |_| {
|
|
observed.fetch_add(1, Ordering::AcqRel);
|
|
}));
|
|
let folder = InventoryFolder::new(UUID::new_with_u_int64(7)?)?;
|
|
inventory.update_node_for(&folder)?;
|
|
assert_eq!(calls.load(Ordering::Acquire), 1);
|
|
drop(subscription);
|
|
client.dispose_with_method()?;
|
|
# Ok::<(), libremetaverse::Error>(())
|
|
```
|
|
|
|
## Threading and callbacks
|
|
|
|
Public shared managers use `Arc`, atomics, mutexes, channels, and cancellation
|
|
tokens rather than a CLR synchronization context. A type being cloneable does
|
|
not make a callback re-entrant: keep handlers short, move expensive work to a
|
|
bounded queue, and never block an async runtime thread waiting for itself.
|
|
Download, inventory, client, and voice diagnostics expose owned task/queue
|
|
counts for shutdown checks. The [concurrency audit](concurrency-hardening.md)
|
|
defines the exact baseline and soak thresholds.
|
|
|
|
## Security boundaries
|
|
|
|
The released graph contains no CLR host, .NET assembly loader, subprocess RPC,
|
|
or foreign `LibreMetaverse` bridge. HTTP uses rustls. Untrusted LLSD, archive,
|
|
image, packet, event, and signaling inputs have explicit size/depth limits.
|
|
Secrets belong in operation arguments or environment variables, never in
|
|
diagnostics, evidence, filenames, command lines, or committed fixtures.
|
|
|
|
Offline/fake modes are real deterministic executions, not skipped live tests.
|
|
Live login and every mutating smoke action require their own opt-in and literal
|
|
confirmation. See the [live-grid boundary](live-grid-smoke.md).
|
|
|
|
## Native prerequisites
|
|
|
|
The default client build needs no image-codec system library. Optional native
|
|
features require:
|
|
|
|
- `OpenJPEG` 2.5.4 or newer for `jpeg2000`;
|
|
- no native prerequisite for the pure-Rust `rust-j2k` feature;
|
|
- the rust-skia prerequisites/cache for `skia`;
|
|
- system libopus for WebRTC voice and the `libremetaverse-opus` adapter;
|
|
- ALSA development headers on Linux, `CoreAudio` on macOS, or WASAPI on Windows
|
|
when `real-audio` enables CPAL;
|
|
- the Vorbis encoder build prerequisites for `vorbis`.
|
|
|
|
Discovery uses `pkg-config` on Unix/macOS and `vcpkg` on Windows MSVC where
|
|
applicable. The adapters remain cross-platform; Gitea workflows intentionally
|
|
run only on `ubuntu-latest`.
|
|
|
|
## Live `OpenSim` setup
|
|
|
|
Use a dedicated `OpenSim` account and put credentials in the workspace `.env` or
|
|
the process environment. The login URL is used directly; no Second Life host is
|
|
substituted.
|
|
|
|
```text
|
|
GRID_USER=First Last
|
|
GRID_PASSWORD=...
|
|
GRID_LOGIN_URL=https://your-opensim.example/login
|
|
```
|
|
|
|
Start with the credential-safe audit and fake smoke, then opt into live login:
|
|
|
|
```sh
|
|
cargo run -p libremetaverse-programs --bin live-grid-smoke -- --audit-only
|
|
cargo run -p libremetaverse-programs --bin live-grid-smoke -- --fake
|
|
cargo run -p libremetaverse-programs --bin live-grid-smoke -- \
|
|
--allow-live-login --confirm-live-login LOGIN
|
|
```
|
|
|
|
Chat, movement, and reversible inventory each require additional confirmations
|
|
documented in the [`OpenSim` live-grid guide](live-grid-smoke.md). A test that
|
|
needs live credentials must fail clearly when they are absent; it must not
|
|
silently skip.
|
|
|
|
## Programs and operational tools
|
|
|
|
All pinned upstream programs have native Rust targets and deterministic offline
|
|
tests. Their arguments, exit statuses, fake/live boundaries, and focused test
|
|
commands are linked from the [program operations manual](../programs/README.md):
|
|
|
|
- [`osd-inspector`](../programs/README.md#osdinspector)
|
|
- [`simple-bot`](../programs/README.md#simplebot)
|
|
- [`packet-dump`](../programs/README.md#packetdump)
|
|
- [`prim-inspector`](../programs/README.md#priminspector)
|
|
- [`inventory-explorer`](../programs/README.md#inventoryexplorer)
|
|
- [`irc-gateway`](../programs/README.md#ircgateway)
|
|
- [`test-client`](../programs/README.md#testclient)
|
|
- [`vivox-test`](../programs/README.md#vivoxtest)
|
|
- [`webrtc-test`](../programs/README.md#webrtctest)
|
|
|
|
For services, run the client under an external supervisor, propagate shutdown
|
|
cancellation, bound queues/files, collect sanitized evidence, and call logout
|
|
before disposal. The tool never invents persistence or retry policy on behalf of
|
|
the application.
|
|
|
|
## Documentation validation
|
|
|
|
The complete documentation gate is:
|
|
|
|
```sh
|
|
cargo run --locked -p metacrate-ci-matrix -- documentation-audit \
|
|
--evidence /tmp/metacrate-documentation.json
|
|
RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps --locked -j 1
|
|
cargo test --locked -p libremetaverse --doc -j 1
|
|
cargo test --locked -p libremetaverse --examples -j 1
|
|
cargo run --locked -p libremetaverse --example offline_client
|
|
cargo run --locked -p libremetaverse --example cancellation
|
|
```
|
|
|
|
The generated [coverage report](../api/DOCUMENTATION-COVERAGE.md) proves that
|
|
every mapped member/type retains its exact C# concept ID and mapping context,
|
|
all public crates have root docs, all local Markdown links resolve, all programs
|
|
are linked, and every Rust fence in this guide is compiled as a doctest.
|