This guide is for existing Rustler crates whose domain implementation remains in
Rust. For a new ordinary NIF, start with
RustQ.Native and defnif instead; they derive the crate,
wrapper, codecs, stubs, initialization, build, and loading together.
Use the helpers below when an existing crate intentionally keeps Cargo and runtime ownership but wants structural generation for repetitive Rustler glue. For generator architecture and checked output, also read Designing RustQ Generators and Generating Rust.
Keep one signature source
A handwritten crate can keep domain implementation functions with an _impl
suffix:
fn parse_nif_impl<'a>(env: Env<'a>, source: &str) -> NifResult<Term<'a>> {
// domain implementation
}Declare only export policy in Elixir:
nifs = [
parse_nif: [],
compile_nif: [attrs: [A.attr(:allow, [A.path([:clippy, :too_many_arguments])])]]
]
rust "native/my_nif/src/generated_nifs.rs" do
RustQ.Rustler.Nif.wrappers_from_source(
"native/my_nif/src/lib.rs",
nifs,
schedule: :dirty_cpu
)
end
generate "lib/my_app/native/generated_stubs.ex" do
content(
RustQ.Rustler.Nif.stubs_from_source(
"native/my_nif/src/lib.rs",
nifs,
MyApp.Native.GeneratedStubs
)
)
endRustQ.Syn reads arguments, returns, lifetimes, and Rustler's injected Env
from the Rust source. The same metadata drives Rust wrappers and Elixir stub
arities, so the export manifest does not repeat signatures.
Keep human-facing @specs in the runtime module and install the generated stubs
once:
defmodule MyApp.Native do
@spec parse_nif(String.t()) :: {:ok, map()} | {:error, term()}
use MyApp.Native.GeneratedStubs
endIf the implementation itself can be expressed cleanly as Rusty-Elixir, prefer
defnif rather than retaining an _impl wrapper merely for appearance.
Atom registries
Discover atom calls structurally and emit one registry:
atoms =
"native/my_nif/src/*.rs"
|> Path.wildcard()
|> Enum.flat_map(fn path ->
path |> File.read!() |> RustQ.Syn.atom_references!()
end)
|> Enum.uniq()
|> Enum.sort()
rust "native/my_nif/src/generated_atoms.rs" do
RustQ.Rustler.Atom.declaration(atoms)
endExclude generated files from discovery. When a schema or manifest introduces additional keys, derive those keys from that same source so generation reaches a stable result in one pass. Do not maintain a second handwritten atom registry.
Term encoders
RustQ.Rustler.Term.encoder/2 creates a structural rustler::Encoder
implementation. Simple fields are atoms; {key, field} renames a key:
RustQ.Rustler.Term.encoder(:EncodedLocation,
fields: [:start, {:end_, :end}, :line]
)Lifetime-bearing adapters can declare target lifetimes and structural field transformations:
RustQ.Rustler.Term.encoder(:EncodedError,
target_lifetimes: [:_],
fields: [
:message,
code: [when_some: true, via: :as_str]
]
)Common field operations include:
field: [:result, :code]— nested field pathvia: :as_str— zero-argument method before encodingwith: :encode_value— helper called with the environment and valueborrow: false— pass a helper value without adding&when_some: true— omit the entry when an option isNoneoptional: [wrap: :EncodedValue]— wrapSome, encodeNoneas nilmap: [wrap: :EncodedValue]— wrap collection elementsmap: [convert: :EncodedValue]— map throughFromfallback: [...]— generate a structural fallback chain
These options represent typed operations. If a transformation becomes domain logic, move it to a named Rust or Rusty-Elixir helper rather than embedding raw expressions in field metadata.
Resources, options, and schemas
RustQ.Rustler.Resource, RustQ.Rustler.Opts, and
RustQ.Rustler.Schema provide structural generation for larger existing
surfaces. Use them when a schema is already the source of truth. Do not add a
schema DSL solely to avoid writing a small, clear @type used by
RustQ.Native.
Prefer one structural owner for fields and variants. Rust declarations, term codecs, atom keys, and dispatchers should be projections of that owner rather than parallel tables.
CI
Check generated output and both language toolchains:
ci: [
"rustq.gen --check",
"compile --warnings-as-errors",
"test"
]Also run cargo fmt --check, cargo check, and cargo clippy -- -D warnings
for every generated native crate. Generated-source freshness alone does not
prove that imported Rust metadata and wrappers still compile together.