guides/resources.md

# Resource Management

This guide covers NIF resources - native data structures managed by the BEAM's garbage collector.

## What Are Resources?

Resources are reference-counted native memory blocks that:

- Are garbage collected by the BEAM VM
- Can have destructor callbacks for cleanup
- Are opaque to Elixir code (cannot be inspected)
- Are type-safe (each resource has a registered type)

Use resources when you need to:
- Store native data structures across NIF calls
- Wrap handles to external libraries (file handles, network connections)
- Manage memory that requires cleanup

## Basic Usage

### 1. Register the Resource Type

Resource types must be registered in your `on_load` callback:

```c3
import c3nif::resource;

ErlNifResourceType* g_my_resource_type;

fn CInt on_load(
    ErlNifEnv* raw_env,
    void** priv,
    ErlNifTerm load_info
) {
    Env e = env::wrap(raw_env);

    ErlNifResourceType*? rt = resource::register_type(
        &e,
        "MyResource",
        &my_resource_destructor
    );

    if (catch err = rt) {
        return 1;  // Failed
    }

    g_my_resource_type = rt;
    return 0;  // Success
}
```

### 2. Define Your Data Structure

```c3
struct MyData {
    int value;
    char* name;
    bool initialized;
}
```

### 3. Define the Destructor

```c3
fn void my_resource_destructor(
    ErlNifEnv* env,
    void* obj
) {
    MyData* data = (MyData*)obj;

    // Cleanup any allocated memory
    if (data.name != null) {
        allocator::free(data.name);
    }

    // The resource memory itself is freed automatically
}
```

### 4. Create Resources

```c3
<* nif: arity = 1 *>
fn ErlNifTerm create_resource(
    ErlNifEnv* raw_env,
    CInt argc,
    ErlNifTerm* argv
) {
    Env e = env::wrap(raw_env);
    Term arg = term::wrap(argv[0]);

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

    // Allocate the resource
    void*? ptr = resource::alloc(my_resource_type, MyData.sizeof);
    if (catch err = ptr) {
        return term::make_error_atom(&e, "alloc_failed").raw();
    }

    // Initialize the data
    MyData* data = (MyData*)ptr;
    data.value = value;
    data.name = null;
    data.initialized = true;

    // Create term and release our reference
    Term result = resource::make_term(&e, ptr);
    resource::release(ptr);  // Term now owns the reference

    return result.raw();
}
```

### 5. Access Resources

```c3
<* nif: arity = 1 *>
fn ErlNifTerm get_value(
    ErlNifEnv* raw_env,
    CInt argc,
    ErlNifTerm* argv
) {
    Env e = env::wrap(raw_env);
    Term arg = term::wrap(argv[0]);

    // Extract the resource
    void*? ptr = resource::get(my_resource_type, &e, arg);
    if (catch err = ptr) {
        return term::make_badarg(&e).raw();
    }

    MyData* data = (MyData*)ptr;
    return term::make_int(&e, data.value).raw();
}
```

## Reference Counting

Resources use reference counting for lifetime management:

| Operation | Effect |
|-----------|--------|
| `alloc()` | Creates resource with ref count = 1 |
| `make_term()` | Increments ref count (+1) |
| `release()` | Decrements ref count (-1) |
| `keep()` | Increments ref count (+1) |

### Standard Pattern

```c3
// Allocate (ref count = 1)
void* ptr = resource::alloc(my_type, size)!;

// Initialize...
MyStruct* data = (MyStruct*)ptr;
data.field = value;

// Create term (ref count = 2)
Term t = resource::make_term(&e, ptr);

// Release our reference (ref count = 1, term owns it)
resource::release(ptr);

// Return the term
return t.raw();
```

### Keeping References in Native Code

If you need to store a resource pointer that survives beyond the NIF call:

```c3
// Store resource pointer in native storage
void* ptr = resource::get(my_type, &e, arg)!;
resource::keep(ptr);  // Increment ref count
g_my_global_ptr = ptr;  // Now safe to store

// Later, when done:
resource::release(g_my_global_ptr);  // Decrement ref count
g_my_global_ptr = null;
```

## Destructor Callbacks

Destructors are called when the resource's reference count reaches zero:

```c3
fn void my_destructor(ErlNifEnv* env, void* obj) {
    MyData* data = (MyData*)obj;

    // Free any nested allocations
    if (data.buffer != null) {
        allocator::free(data.buffer);
    }

    // Close any handles
    if (data.file_handle != null) {
        close_file(data.file_handle);
    }

    // The resource memory itself is freed automatically by the BEAM
}
```

### Destructor Rules

1. **Timing is non-deterministic** - Depends on garbage collection
2. **Runs on arbitrary scheduler** - Could be any thread
3. **Keep it fast** - Don't block or do heavy computation
4. **Limited env** - Can't create terms with the destructor's env
5. **Can send messages** - Use `enif_alloc_env()` for message construction

### Sending Messages from Destructors

```c3
fn void cleanup_destructor(ErlNifEnv* env, void* obj) {
    MyData* data = (MyData*)obj;

    if (data.notify_pid_valid) {
        // Create a private environment for the message
        ErlNifEnv* msg_env = erl_nif::enif_alloc_env();

        // Build message in the private environment
        Env e = env::wrap(msg_env);
        Term msg = term::make_tuple_from_array(&e, (ErlNifTerm[2]){
            term::make_atom(&e, "resource_destroyed").raw(),
            term::make_int(&e, data.id).raw()
        }[0:2]);

        // Send the message
        erl_nif::enif_send(null, &data.notify_pid, msg_env, msg.raw());

        // Free the private environment
        erl_nif::enif_free_env(msg_env);
    }
}
```

## Process Monitoring

Resources can monitor Erlang processes and receive callbacks when they die:

### Registration with Down Callback

```c3
fn void my_down_callback(
    ErlNifEnv* env,
    void* obj,
    ErlNifPid* pid,
    ErlNifMonitor* monitor
) {
    MyData* data = (MyData*)obj;
    // Handle the process death
    data.owner_alive = false;
}

fn CInt on_load(ErlNifEnv* raw_env, void** priv, ErlNifTerm load_info) {
    Env e = env::wrap(raw_env);

    // Use register_type_full for down callback support
    ErlNifResourceTypeInit init = {
        .dtor = &my_destructor,
        .stop = null,
        .down = &my_down_callback,
        .members = 3,  // Must be >= 3 for down callback
        .dyncall = null
    };

    ErlNifResourceType*? rt = resource::register_type_full(
        &e,
        "MonitoredResource",
        &init
    );

    // ...
}
```

### Setting Up a Monitor

```c3
<* nif: arity = 2 *>
fn ErlNifTerm monitor_owner(
    ErlNifEnv* raw_env,
    CInt argc,
    ErlNifTerm* argv
) {
    Env e = env::wrap(raw_env);

    void* ptr = resource::get(monitored_type, &e, term::wrap(argv[0]))!;
    ErlNifPid? owner_pid = term::wrap(argv[1]).get_local_pid(&e);
    if (catch err = owner_pid) {
        return term::make_badarg(&e).raw();
    }

    MyData* data = (MyData*)ptr;

    // Start monitoring the process
    if (!resource::monitor_process(&e, ptr, &owner_pid, &data.monitor)) {
        return term::make_error_atom(&e, "monitor_failed").raw();
    }

    data.owner_pid = owner_pid;
    data.owner_alive = true;

    return term::make_atom(&e, "ok").raw();
}
```

### Canceling a Monitor

```c3
// In NIF:
if (!resource::demonitor_process(&e, ptr, &data.monitor)) {
    // Already triggered or invalid
}
```

## Thread Safety

### Safe Operations (Any Thread)

- `resource::alloc()` / `resource::release()` / `resource::keep()`
- `resource::monitor_process()` / `resource::demonitor_process()`
- Reading immutable resource fields

### Unsafe Without Synchronization

- Modifying resource fields from multiple threads
- Reading mutable fields while another thread writes

### Synchronization Strategies

For mutable resource state:

```c3
struct ThreadSafeData {
    // Use atomics for simple values
    int counter;  // Access with atomic operations

    // Or use a mutex for complex state
    // (requires platform-specific implementation)
}

// Atomic increment example
fn void increment_counter(ThreadSafeData* data) {
    // Use C3's atomic intrinsics
    @atomic_add(&data.counter, 1);
}
```

## Complete Example

```c3
module counter_resource;

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

struct Counter {
    int value;
}

ErlNifResourceType* g_counter_type;

fn void counter_destructor(ErlNifEnv* env, void* obj) {
    // Nothing to clean up for this simple struct
}

fn CInt on_load(ErlNifEnv* raw_env, void** priv, ErlNifTerm load_info) {
    Env e = env::wrap(raw_env);

    ErlNifResourceType*? rt = resource::register_type(
        &e,
        "Counter",
        &counter_destructor
    );

    if (catch err = rt) {
        return 1;
    }

    g_counter_type = rt;
    return 0;
}

<* nif: arity = 1 *>
fn ErlNifTerm new_counter(
    ErlNifEnv* raw_env,
    CInt argc,
    ErlNifTerm* argv
) {
    Env e = env::wrap(raw_env);

    int? initial = term::wrap(argv[0]).get_int(&e);
    if (catch err = initial) {
        return term::make_badarg(&e).raw();
    }

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

    Counter* counter = (Counter*)ptr;
    counter.value = initial;

    Term result = resource::make_term(&e, ptr);
    resource::release(ptr);

    return result.raw();
}

<* nif: arity = 1 *>
fn ErlNifTerm get_counter(
    ErlNifEnv* raw_env,
    CInt argc,
    ErlNifTerm* argv
) {
    Env e = env::wrap(raw_env);

    void*? ptr = resource::get(counter_type, &e, term::wrap(argv[0]));
    if (catch err = ptr) {
        return term::make_badarg(&e).raw();
    }

    Counter* counter = (Counter*)ptr;
    return term::make_int(&e, counter.value).raw();
}

<* nif: arity = 2 *>
fn ErlNifTerm increment_counter(
    ErlNifEnv* raw_env,
    CInt argc,
    ErlNifTerm* argv
) {
    Env e = env::wrap(raw_env);

    void*? ptr = resource::get(counter_type, &e, term::wrap(argv[0]));
    if (catch err = ptr) {
        return term::make_badarg(&e).raw();
    }

    int? amount = term::wrap(argv[1]).get_int(&e);
    if (catch err = amount) {
        return term::make_badarg(&e).raw();
    }

    Counter* counter = (Counter*)ptr;
    counter.value += amount;

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

Elixir usage:

```elixir
counter = MyApp.Counter.new_counter(0)
MyApp.Counter.increment_counter(counter, 5)
MyApp.Counter.get_counter(counter)  # => 5
```

## Best Practices

1. **Always release after make_term** - Prevents memory leaks

2. **Initialize all fields** - Destructors may be called on partially initialized resources

3. **Check for null in destructors** - Handle cleanup of optional fields safely

4. **Use process monitors for cleanup** - Don't rely solely on GC timing

5. **Keep destructors fast** - Heavy cleanup should be done in a dedicated process

6. **Document thread safety** - Be explicit about what's safe to call concurrently