NIF Functions
View SourceThis guide covers how to define and configure NIF functions in C3nif.
NIF Function Signature
Every NIF function must follow this exact signature:
fn ErlNifTerm function_name(
ErlNifEnv* raw_env,
CInt argc,
ErlNifTerm* argv
)raw_env- The NIF environment handleargc- Number of arguments passed (should match your declared arity)argv- Array of argument terms
NIF Annotations
NIF functions are marked with the <* nif: ... *> annotation in C3 doc comment syntax:
<* nif: arity = 2 *>
fn ErlNifTerm add(
ErlNifEnv* raw_env,
CInt argc,
ErlNifTerm* argv
) {
// implementation
}Available Annotation Options
| Option | Type | Description |
|---|---|---|
arity | integer | Number of Elixir arguments (required) |
name | string | Custom Elixir function name |
dirty | cpu or io | Run on dirty scheduler |
Custom Function Names
Use name to expose a different name to Elixir:
<* nif: name = "my_function", arity = 1 *>
fn ErlNifTerm internal_impl(
ErlNifEnv* raw_env,
CInt argc,
ErlNifTerm* argv
) {
// ...
}In Elixir, this becomes my_function/1, not internal_impl/1.
Dirty Schedulers
For long-running operations, use dirty schedulers:
<* nif: arity = 1, dirty = cpu *>
fn ErlNifTerm cpu_intensive(
ErlNifEnv* raw_env,
CInt argc,
ErlNifTerm* argv
) {
// CPU-bound work
}
<* nif: arity = 1, dirty = io *>
fn ErlNifTerm io_bound(
ErlNifEnv* raw_env,
CInt argc,
ErlNifTerm* argv
) {
// I/O operations
}See the Dirty Schedulers guide for more details.
Elixir Stubs
Every NIF needs a corresponding Elixir function stub:
defmodule MyModule do
use C3nif, otp_app: :my_app
~n"""
// ... C3 code ...
"""
# Stubs - called if NIF not loaded
def add(_a, _b), do: :erlang.nif_error(:nif_not_loaded)
def multiply(_a, _b), do: :erlang.nif_error(:nif_not_loaded)
endThese stubs:
- Define the function arity for the Erlang VM
- Provide a fallback if the NIF fails to load
- Can include documentation via
@doc
Accessing Arguments
Wrap raw arguments before use:
Env e = env::wrap(raw_env);
Term arg0 = term::wrap(argv[0]);
Term arg1 = term::wrap(argv[1]);Using Safety Helpers
C3nif provides helpers for safer argument access:
import c3nif::safety;
fn ErlNifTerm my_nif(
ErlNifEnv* raw_env,
CInt argc,
ErlNifTerm* argv
) {
Env e = env::wrap(raw_env);
// Validate argument count
if (safety::require_argc(argc, 2)) |err| {
return err.raw();
}
// Get typed arguments
int? a = safety::require_int(&e, argv, 0);
if (catch err = a) {
return term::make_badarg(&e).raw();
}
int? b = safety::require_int(&e, argv, 1);
if (catch err = b) {
return term::make_badarg(&e).raw();
}
return term::make_int(&e, a + b).raw();
}Return Values
Always return an ErlNifTerm. Use the .raw() method on wrapped terms:
// Return an integer
return term::make_int(&e, 42).raw();
// Return an atom
return term::make_atom(&e, "ok").raw();
// Return a tuple {:ok, value}
return term::make_ok_tuple(&e, term::make_int(&e, result)).raw();
// Return an error tuple {:error, reason}
return term::make_error_tuple(&e, term::make_atom(&e, "invalid_input")).raw();
// Signal argument error (raises ArgumentError in Elixir)
return term::make_badarg(&e).raw();Lifecycle Callbacks
C3nif automatically detects on_load and on_unload callbacks:
// Called when NIF is loaded
fn CInt on_load(
ErlNifEnv* env,
void** priv,
ErlNifTerm load_info
) {
// Initialize resources, state, etc.
return 0; // 0 = success
}
// Called when NIF is unloaded
fn void on_unload(
ErlNifEnv* env,
void* priv
) {
// Cleanup resources
}These callbacks are detected by their function names and signatures.
Multiple NIFs in One Module
You can define multiple NIFs in a single module:
defmodule MyApp.Math do
use C3nif, otp_app: :my_app
~n"""
module math;
import c3nif;
import c3nif::erl_nif;
import c3nif::env;
import c3nif::term;
<* nif: arity = 2 *>
fn ErlNifTerm add(...) { /* ... */ }
<* nif: arity = 2 *>
fn ErlNifTerm subtract(...) { /* ... */ }
<* nif: arity = 2 *>
fn ErlNifTerm multiply(...) { /* ... */ }
<* nif: arity = 2 *>
fn ErlNifTerm divide(...) { /* ... */ }
"""
def add(_a, _b), do: :erlang.nif_error(:nif_not_loaded)
def subtract(_a, _b), do: :erlang.nif_error(:nif_not_loaded)
def multiply(_a, _b), do: :erlang.nif_error(:nif_not_loaded)
def divide(_a, _b), do: :erlang.nif_error(:nif_not_loaded)
endCode Organization
For larger NIFs, split your C3 code into multiple ~n blocks:
defmodule MyApp.Nif do
use C3nif, otp_app: :my_app
# Module declaration and imports
~n"""
module mynif;
import c3nif;
import c3nif::erl_nif;
import c3nif::env;
import c3nif::term;
"""
# Helper functions
~n"""
fn int helper_function(int x) {
return x * 2;
}
"""
# NIF implementations
~n"""
<* nif: arity = 1 *>
fn ErlNifTerm process(
ErlNifEnv* raw_env,
CInt argc,
ErlNifTerm* argv
) {
Env e = env::wrap(raw_env);
// ...
}
"""
def process(_data), do: :erlang.nif_error(:nif_not_loaded)
endAll ~n blocks are concatenated before compilation.
External Source Files
For even better organization, use the :c3_sources option to include external C3 files:
defmodule MyApp.Nif do
use C3nif,
otp_app: :my_app,
c3_sources: [
"c3_src/helpers.c3",
"c3_src/utils/**/*.c3"
]
~n"""
module mynif;
import c3nif;
import c3nif::erl_nif;
import helpers; // External module
<* nif: arity = 1 *>
fn ErlNifTerm process(...) { /* ... */ }
"""
def process(_data), do: :erlang.nif_error(:nif_not_loaded)
endExternal sources support glob patterns (**/*.c3) for matching multiple files.
Best Practices
Keep NIFs short - NIFs should complete quickly (< 1ms). Use dirty schedulers for longer operations.
Validate arguments early - Check types and ranges before doing work.
Handle all error cases - Return badarg or error tuples for invalid input.
Document your NIFs - Use
@docon Elixir stubs to document behavior.Test thoroughly - NIFs can crash the VM if written incorrectly.