Rust

Article & Tutorial

Unsafe Rust, FFI and WebAssembly: Advanced Rust Boundaries Explained

Learn unsafe Rust, raw pointers, FFI, ABI boundaries and WebAssembly while keeping unsafe code isolated, documented and auditable.

Unsafe Rust FFI and WebAssembly article illustration
Unsafe Rust FFI and WebAssembly article illustration
Contents

1. unsafe does not disable the borrow checker

An unsafe block permits a small set of additional operations, but most Rust rules still apply. The responsibility shifts to you to uphold the invariants the compiler cannot verify.

2. Raw pointers

bash
fn main() { let mut value = 10; let read = &value as *const i32; let write = &mut value as *mut i32; unsafe { println!("{}", *read); *write = 20; } }

Raw pointers may be null, dangling or unaligned. Dereferencing them requires unsafe.

3. Unsafe functions

bash
unsafe fn read_address(ptr: *const u8) -> u8 { unsafe { *ptr } }

Document the safety contract: what the caller must guarantee about validity, alignment, lifetime and aliasing.

4. Build safe wrappers

A strong pattern is a very small unsafe core surrounded by a safe API:

bash
pub fn first_byte(data: &[u8]) -> Option<u8> { if data.is_empty() { None } else { unsafe { Some(*data.as_ptr()) } } }

In this example the unsafe operation is unnecessary because safe indexing exists, but it demonstrates the boundary: validate invariants before entering unsafe code.

5. Unsafe traits

An unsafe trait has invariants that implementations must uphold. Marking an implementation unsafe means the implementer asserts those requirements are satisfied. This is advanced library-author territory.

6. Foreign Function Interface (FFI)

FFI allows Rust to call C-compatible libraries and allows external code to call Rust through a stable ABI boundary.

bash
unsafe extern "C" { fn abs(input: i32) -> i32; } fn main() { let value = unsafe { abs(-10) }; println!("{value}"); }

7. Export a Rust function to C

bash
#[unsafe(no_mangle)] pub extern "C" fn add(a: i32, b: i32) -> i32 { a + b }

Exact attributes and ABI requirements depend on the Rust edition/toolchain and target. Keep the exported surface small and test it from the consuming language.

8. FFI-safe data types

Do not pass arbitrary Rust types such as String or Vec directly through a C ABI. Use C-compatible primitives, explicit pointer/length pairs or #[repr(C)] structures designed for the boundary.

9. Ownership across FFI

Decide who allocates and who frees every resource. A common design exports paired create/free functions so memory is released by the same allocator/runtime that created it.

10. Validate external pointers

Foreign callers can give you null or invalid pointers. Raw-pointer validation is limited—checking for null does not prove memory is valid—so FFI APIs should minimize pointer complexity and document strict contracts.

11. Panic boundaries

Do not allow unwinding behavior to cross an ABI boundary that does not support it. Convert failures into explicit error codes/results appropriate for the foreign API.

12. WebAssembly overview

Rust can compile to WebAssembly targets, allowing selected Rust logic to run in browsers, server-side Wasm runtimes and other sandboxed environments.

bash
rustup target add wasm32-unknown-unknown cargo build --target wasm32-unknown-unknown --release

13. wasm-bindgen

For browser JavaScript interoperability, the wasm-bindgen ecosystem generates bindings that make it easier to expose Rust functions and work with JavaScript values.

14. Good Wasm candidates

  • CPU-heavy parsing/transformations
  • Portable domain logic
  • Image/data processing
  • Cryptographic operations through reviewed libraries
  • Reusable logic shared across supported Wasm hosts

15. What not to assume about Wasm

WebAssembly does not automatically make an application faster. Boundary conversion, host APIs, payload copying and deployment constraints all matter. Benchmark the real workload.

16. Audit unsafe code

  • Keep unsafe blocks as small as possible.
  • Add comments describing safety invariants.
  • Test edge cases and invalid inputs.
  • Use established crates instead of reimplementing low-level primitives.
  • Review unsafe changes carefully in code review.

17. Mini lab

Create a tiny Rust library with one safe function. Export a C-compatible addition function, build it as a library and inspect its symbol. Separately compile a pure calculation function to wasm32-unknown-unknown. Keep all unsafe code limited to the FFI boundary.

Key takeaways

unsafe is an explicit trust boundary, not a performance switch. Use it only when necessary, document invariants, isolate it behind safe APIs and treat FFI/Wasm boundaries as places where validation and ownership must be especially clear.

Did this help?

Yes No

Join the discussion

0 Comments

Ask questions, add context, or share what worked.

Sign in to join the discussion

No comments yet. Start the discussion.

More from Melochip Editorial

Related articles

Stay in the loop. Get new Melochip articles

Receive Melochip article updates when new articles are available. Explicit opt-in only.

No pre-checked boxes. Unsubscribe support is part of the subscriber foundation.