Current section

Files

Jump to
rustq guides zero-rust-nifs.md
Raw

guides/zero-rust-nifs.md

# Zero-handwritten-Rust NIFs
`RustQ.Native` generates, builds, and loads a Rustler crate from typed
Rusty-Elixir. It is the default starting point when RustQ should own an ordinary
NIF boundary.
```elixir
defmodule MyApp.Native do
use RustQ.Native, crates: [crc32fast: "1"]
alias RustQ.Type, as: R
@spec checksum(String.t()) :: R.u32()
defnif checksum(value), do: Crc32fast.hash(value.as_bytes())
end
```
The module above needs no checked-in `.rs` file, `Cargo.toml`, NIF registry,
Elixir stub module, or loader. RustQ generates those pieces under the Mix build
directory and keeps the generated crate available for inspection.
For authoring conventions and inference, read
[Using RustQ Well](using-rustq-well.md). This guide focuses on crate ownership
and the NIF boundary.
## Public entrypoints and helpers
Use each function form for one responsibility:
- `defnif` — public BEAM↔Rust entrypoint
- `defrust` — generated public Rust helper
- `defrustp` — generated private Rust helper
All three use real `@spec`s. Ordinary Elixir macros are the preferred way to
reuse Rusty-Elixir source.
```elixir
@spec add(integer(), integer()) :: integer()
defnif add(left, right), do: add_impl(left, right)
@spec add_impl(integer(), integer()) :: integer()
defrustp add_impl(left, right), do: left + right
```
## What RustQ owns
For a normal `RustQ.Native` module, RustQ owns:
- the generated Cargo package and source
- declared Cargo dependencies
- Rustler attributes and initialization
- Elixir exports and native loading
- directional argument and return codecs
- generated atoms, structural types, and resources
- native compilation and installation into the application
Use `crates:` when generated code calls an external crate:
```elixir
use RustQ.Native,
crates: [
crc32fast: "1",
serde_json: [version: "1", features: ["preserve_order"]]
]
```
RustQ derives Rust module aliases from crate names. Cargo versions and features
remain explicit because they are release policy, not inference.
## Boundary types
RustQ derives directional codecs from `@spec` and `@type`. Supported boundary
families include:
- integers, floats, booleans, atoms, strings, and binaries
- tuples and homogeneous lists
- options and tagged results
- typed maps and Elixir structs
- unit enums and structural unions
- Elixir exceptions
- explicitly declared resources
Elixir-facing, Rust-internal, NIF-input, and NIF-output representations are
related but not assumed identical. For example, an Elixir binary can decode as a
borrowed Rustler binary, be used internally as bytes, and encode from owned
bytes.
A structural map type can own the codec for a NIF value:
```elixir
@type point :: %{required(:x) => float(), required(:y) => float()}
@spec scale(point(), float()) :: point()
defnif scale(point, factor) do
%{x: point.x * factor, y: point.y * factor}
end
```
## Resources
Wrap a structural state type with `R.resource/1` to derive registration and the
`ResourceArc` boundary:
```elixir
@type counter_state :: %{required(:value) => integer()}
@type counter :: R.resource(counter_state())
@spec new_counter(integer()) :: counter()
defnif new_counter(value), do: %{value: value}
@spec counter_value(counter()) :: integer()
defnif counter_value(counter), do: counter.value
```
`R.resource/1` does not choose mutation, synchronization, ownership, or thread
safety. Those decisions remain explicit and may require a deliberately designed
state type or nearby handwritten Rust.
## Scheduling
A `defnif` uses the normal BEAM scheduler by default. Mark only entrypoints that
need dirty scheduling:
```elixir
@nif schedule: :dirty_cpu
@spec resize_image(binary()) :: binary()
defnif resize_image(image), do: resize_impl(image)
@nif schedule: :dirty_io
@spec read_device(String.t()) :: binary()
defnif read_device(path), do: read_device_impl(path)
```
Use `:dirty_cpu` for long-running CPU-bound work and `:dirty_io` for native work
that may block on IO. The attribute applies only to the declaration immediately
following it. RustQ renders the Rustler `"DirtyCpu"` and `"DirtyIo"` options;
those strings are accepted directly, but atoms are the preferred spelling.
RustQ does not infer scheduling from a function body. Short NIFs should normally
stay on the normal scheduler.
## Existing and precompiled crates
An existing crate can keep Cargo, initialization, loading, and release ownership
while RustQ prepares ABI items:
```elixir
defmodule MyApp.NativeItems do
use RustQ.Native,
build: false,
load: false,
rust_sources: ["native/my_app/src/lib.rs"]
alias RustQ.Type, as: R
@spec decode(term()) :: R.nif_result(term())
defnif decode(value), do: decode_impl(nif_env(), value)
end
```
`RustQ.Native.items/1` returns the prepared functions, codecs, and resource
items for the owning crate to splice. `RustQ.Native.source/1` returns the same
prepared source when an external system needs text.
`nif_env/0` injects `Env<'a>` into generated Rust without adding an argument to
the public BEAM function. Use this mode for source-built or precompiled domain
crates that should not depend on RustQ in production.
## Supported Rusty-Elixir
`defnif` and `defrust` share the same lowering surface. It includes multiple
clauses, patterns, guards, recursion, `case`, conditionals, `with`,
comprehensions, closures, pipelines, ordinary macro expansion, and a
semantics-preserving subset of Kernel, Enum, List, typed Map, String, Tuple, and
Range operations.
The subset is intentionally semantic, not name-based. RustQ rejects operations
that cannot preserve Elixir behavior instead of silently choosing a similar
Rust method. Dynamic or descending ranges, unrestricted dynamic maps,
grapheme-counting `String.length/1`, process semantics, full protocols, async
Rust, IO/network/process APIs, and the complete bitstring grammar remain
explicit adapters or Rust boundaries.
See [Using RustQ Well](using-rustq-well.md) for the authoring ladder and
inference rules. The API reference for `RustQ.Meta` documents individual forms.
## Testing
Use the packaged assertions instead of searching build directories:
```elixir
use RustQ.Test, async: true
assert rust_source!(MyApp.Native, :add_impl) =~ "fn add_impl"
assert rust_source!(MyApp.Native, :add) =~ ~r/fn add/
assert nif_exported?(MyApp.Native, :add, 2)
assert RustQ.valid?(rust_source!(MyApp.Native), "my_app_native.rs")
```
`rust_source!/2` returns one focused function for ordinary ExUnit assertions,
while `rust_source!/1` returns the complete generated module.
`nif_exported?/3` checks both the Elixir export and generated Rustler attribute.
Use `RustQ.valid?/2` when the test specifically needs Rust syntax validation.
## Explicit policy checklist
Before shipping a native module, review:
- dependency versions and Cargo features
- scheduler choice and blocking behavior
- resource ownership and thread safety
- unsafe operations
- lossy conversions and custom adapters
- platform linking
- precompiled targets and artifact hosting
Zero handwritten bridge Rust means RustQ derives repetitive structure. It does
not mean RustQ guesses safety or deployment decisions.