C3nif

Ergonomic Erlang/Elixir NIFs using C3

C3nif is a library for writing Erlang/Elixir Native Implemented Functions (NIFs) using the C3 programming language. If you know C but find raw NIF code tedious, C3nif gives you a cleaner API with less boilerplate.

Why C3nif?

Quick Example

defmodule MyApp.Math do
  use C3nif, otp_app: :my_app

  ~n"""
  module math_nif;

  import c3nif;
  import c3nif::erl_nif;
  import c3nif::env;
  import c3nif::term;

  fn ErlNifTerm add_one(
      ErlNifEnv* raw_env,
      CInt argc,
      ErlNifTerm* argv
  ) {
      Env e = env::wrap(raw_env);
      Term arg0 = term::wrap(argv[0]);

      int? value = arg0.get_int(&e);
      if (catch err = value) {
          return term::make_badarg(&e).raw();
      }

      return term::make_int(&e, value + 1).raw();
  }
  """

  # Elixir function stub (will be replaced by NIF)
  def add_one(_n), do: :erlang.nif_error(:nif_not_loaded)
end

Installation

Add c3nif to your list of dependencies in mix.exs:

def deps do
  [
    {:c3nif, "~> 0.1.0"}
  ]
end

Ensure you have the C3 compiler installed:

# Install C3 compiler (version 0.7.7 or later required)
# See https://c3-lang.org/getting-started/prebuilt-binaries/

Core Features

Type Conversions

C3nif wraps Erlang terms in a Term type with methods that return optionals on failure:

fn ErlNifTerm double_it(
    ErlNifEnv* raw_env,
    CInt argc,
    ErlNifTerm* argv
) {
    Env e = env::wrap(raw_env);
    Term arg = term::wrap(argv[0]);

    // Returns optional - you handle the error or it won't compile
    int? value = arg.get_int(&e);
    if (catch err = value) {
        return term::make_badarg(&e).raw();
    }

    return term::make_int(&e, value * 2).raw();
}

Environment Management

Process-bound and process-independent environments:

// Process-bound (standard NIF call)
Env e = env::wrap(raw_env);

// Process-independent (for async operations)
env::OwnedEnv? owned = env::new_owned_env();
if (catch err = owned) {
    // Handle allocation failure
}
Env async_env = owned.as_env();
// ... build terms, send messages ...
owned.free();

Term Operations

Type checking, creation, and extraction:

// Type checking
if (arg.is_atom(&e)) { ... }
if (arg.is_list(&e)) { ... }

// Integer operations
Term result = term::make_int(&e, 42);
int? extracted = arg.get_int(&e);

// List operations
Term empty = term::make_empty_list(&e);
Term list = term::make_list_cell(&e, head, tail);

// Map operations
Term map = term::make_new_map(&e);
Term? updated = map.map_put(&e, key, value);

// Comparison (with operator overloading)
if (term1 == term2) { ... }

Resource Management

Resources let you wrap native data structures and pass them to Erlang as opaque references:

import c3nif::resource;

// Define your native struct
struct Counter {
    int value;
}

// Destructor called when resource is garbage collected
fn void counter_dtor(ErlNifEnv* env, void* obj) {
    // Cleanup code here (Counter memory is freed automatically)
}

// Register in on_load callback
fn CInt on_load(ErlNifEnv* env_raw, void** priv, ErlNifTerm load_info) {
    Env e = env::wrap(env_raw);
    resource::register_type(&e, "Counter", &counter_dtor)!!;
    return 0;
}

// Create a resource
fn ErlNifTerm create_counter(ErlNifEnv* env_raw, CInt argc, ErlNifTerm* argv) {
    Env e = env::wrap(env_raw);

    void* ptr = resource::alloc("Counter", Counter.sizeof)!!;
    Counter* c = (Counter*)ptr;
    c.value = 42;

    Term t = resource::make_term(&e, ptr);
    resource::release(ptr);  // Term now owns the reference
    return t.raw();
}

// Use a resource
fn ErlNifTerm get_counter(ErlNifEnv* env_raw, CInt argc, ErlNifTerm* argv) {
    Env e = env::wrap(env_raw);
    Term arg = term::wrap(argv[0]);

    void* ptr = resource::get("Counter", &e, arg)!!;
    Counter* c = (Counter*)ptr;

    return term::make_int(&e, c.value).raw();
}

Memory Allocation

BEAM-tracked memory allocation for use with C3 standard library collections:

import c3nif::allocator;

// Simple allocation
void* ptr = allocator::alloc(1024);
if (!ptr) {
    return term::make_error_atom(&e, "alloc_failed").raw();
}
defer allocator::free(ptr);

// Zero-initialized allocation
void* zeroed = allocator::calloc(256);

// Reallocation (preserves data)
void* grown = allocator::realloc(ptr, 2048);

// With C3 Allocator interface (for collections)
allocator::BeamAllocator beam;
List{int} numbers;
numbers.init(&beam);
defer numbers.free();

Thread Safety: All allocator functions are thread-safe and can be called from any thread (scheduler, dirty scheduler, or user-created).

VM Integration: All allocations are tracked by the BEAM VM and visible in erlang:memory() reports.

Strict Pairing: Memory allocated with allocator::alloc must be freed with allocator::free. Never mix with system malloc/free, binary allocators, or resource allocators.

Supported Types

Erlang/Elixir C3 Type Operations
integer()int, uint, long, ulongmake_int, get_int, etc.
float()doublemake_double, get_double
atom()char*make_atom, make_existing_atom
binary()ErlNifBinarymake_new_binary, inspect_binary
list()ErlNifTerm[]make_list_from_array, get_list_cell
tuple()ErlNifTerm[]make_tuple_from_array, get_tuple
map() - make_new_map, map_put, map_get
reference() - make_ref, is_ref
pid()ErlNifPidget_local_pid
resource()void*resource::alloc, resource::get, resource::make_term

Development

# Clone the repository
git clone https://github.com/tristanperalta/c3nif.git
cd c3nif

# Install dependencies
mix deps.get

# Run tests (automatically compiles C3 library)
mix test

Requirements

You Can Still Crash the VM

C3nif makes NIFs more ergonomic, but it doesn't make them memory-safe. You're still writing native code. Things that will crash your VM:

The !! operator is convenient but dangerous - it panics on error:

// This will crash if allocation fails:
void* ptr = resource::alloc("Counter", Counter.sizeof)!!;

// Prefer explicit handling:
void*? ptr = resource::alloc("Counter", Counter.sizeof);
if (catch err = ptr) {
    return term::make_error_atom(&e, "alloc_failed").raw();
}

Other things to remember:

License

MIT License


Note: This project is in active development. The API may change before version 1.0.0.