# Memory and safety rules
Ownership and lifetime rules for the dotLottie C API - who frees what, the correct destruction order, string lifetimes, and thread safety guarantees.

The C API has a small, consistent set of ownership rules. Following them avoids the leaks and use-after-free bugs that a handle-based FFI invites.

## Who owns what

| Resource                                      | Owned by    | Released by                               |
| --------------------------------------------- | ----------- | ----------------------------------------- |
| The player handle                             | The library | `dotlottie_destroy()`                     |
| A state machine handle                        | The library | `dotlottie_state_machine_release()`       |
| Your software render buffer                   | **You**     | `free()`, after destroying the player     |
| Strings you pass in                           | **You**     | Free any time after the call returns      |
| Strings you read into your buffer             | **You**     | `free()` when you're done                 |
| The marker name from `dotlottie_get_marker()` | The library | Nothing — do not free                     |
| String pointers inside polled events          | The library | Nothing — but see the lifetime note below |

The library never hands you a pointer that you are expected to `free()`. Anything you must free is memory you allocated yourself.

## Destruction order

Two ordering rules matter, and both cause hard-to-diagnose crashes when broken.

**Release state machines before the player.** A state machine borrows the player internally, so the player must outlive it.

```c
dotlottie_state_machine_release(machine);
dotlottie_destroy(player);
```

**Destroy the player before freeing your render buffer.** The player retains the pointer you gave `dotlottie_set_sw_target()` and may write to it during rendering.

```c
dotlottie_destroy(player);
free(buffer);
```

Putting both together, teardown runs in the reverse of setup:

```c
/* setup */
player  = dotlottie_new_player(0);
buffer  = malloc(width * height * sizeof(uint32_t));
dotlottie_set_sw_target(player, buffer, width, height, ABGR8888);
machine = dotlottie_state_machine_load(player, "my-machine");

/* teardown */
dotlottie_state_machine_release(machine);
dotlottie_destroy(player);
free(buffer);
```

## String lifetimes

**Strings you pass in are copied.** The library reads them during the call and keeps its own copy of anything it needs to retain, so a stack buffer or a string you free immediately afterwards is fine:

```c
{
  char marker[64];
  snprintf(marker, sizeof(marker), "loop-%d", index);
  dotlottie_set_marker(player, marker);
}   /* safe: the library copied it */
```

All input strings must be null-terminated and valid UTF-8. Invalid UTF-8 returns `InvalidParameter` rather than causing undefined behavior.

**Strings you read out live in your buffer.** Use the two-call pattern, and remember the reported size already includes the null terminator:

```c
size_t size = 0;
dotlottie_get_theme_id(player, NULL, &size);

char *theme = malloc(size);
dotlottie_get_theme_id(player, theme, NULL);
/* ... */
free(theme);
```

<Callout type="warning" title="Event strings expire on the next poll">
  String pointers inside a `dotlottieStateMachineEvent` — state names, input names, messages — point into library-owned memory that is reused by the following poll call. Copy anything you need to keep before polling again.

  ```c
  dotlottieStateMachineEvent event;
  while (dotlottie_state_machine_poll_event(machine, &event) == 1) {
    if (event.event_type == StateMachineTransition) {
      char *previous = strdup(event.data.transition.previous_state);  /* copy to keep */
      /* ... */
      free(previous);
    }
  }
  ```
</Callout>

## Buffer sizing

`dotlottie_set_sw_target()` requires a buffer of exactly `width × height` 32-bit pixels. There is no stride parameter — rows are contiguous.

```c
uint32_t *buffer = malloc(width * height * sizeof(uint32_t));
```

An undersized buffer is a heap overflow, not a caught error. When you resize a surface, allocate the new buffer, call `dotlottie_set_sw_target()` again with the new dimensions, and only then free the old one.

## Thread safety

Handles are **not** internally synchronized. Treat a `DotLottiePlayer *` the way you would a `FILE *`: fine to use from any one thread, but concurrent calls on the same handle need your own mutex.

| Pattern                                            | Safe |
| -------------------------------------------------- | ---- |
| One player used from one thread                    | Yes  |
| Separate players on separate threads               | Yes  |
| One player from several threads, externally locked | Yes  |
| One player from several threads, unlocked          | No   |

The `threads` argument to `dotlottie_new_player()` is unrelated to this. It sizes the renderer's internal worker pool, which parallelizes rasterization inside a single `dotlottie_render()` call — it does not make the handle safe to share.

Do not read your software buffer while a render is in progress on another thread. With a single-threaded loop this is automatic, since `dotlottie_render()` and `dotlottie_tick()` return only once rendering has finished.

## Null handling

Passing `NULL` where a handle is expected returns `InvalidParameter` rather than crashing, so defensive checks are cheap. Output pointers are validated too: most getters return `InvalidParameter` if the destination is null, and the ones that accept `NULL` do so deliberately — the `buffer`/`size_out` pair in string getters, and the optional `start`/`end` of `dotlottie_get_marker()`.

`dotlottie_tick()`'s `rendered` argument may be `NULL` if you don't care whether a new frame was produced.

## Panics abort the process

Release builds are compiled with `panic = "abort"`. An unrecoverable internal error terminates the process immediately; it cannot be caught on the C side or turned into a result code. This path is reserved for genuine bugs — expected failures like a missing file or malformed animation data are reported as result codes. If you do see an abort, it's worth [filing an issue↗](https://github.com/LottieFiles/dotlottie-rs/issues).

## Checklist

- Every `dotlottie_new_player()` is matched by a `dotlottie_destroy()`
- Every `dotlottie_state_machine_load()` is matched by a `dotlottie_state_machine_release()`, called first
- The software buffer is freed after the player is destroyed, never before
- Buffers from string getters are freed once you're done with them
- Marker names from `dotlottie_get_marker()` are never freed
- Event strings are copied before the next poll
- Concurrent access to one handle is guarded by your own lock

## Learn more

- [How the C API works](/en/runtimes/distributions/native/v0.x/how-the-c-api-works) — handles, results, and feature gating
- [Handle events](/en/runtimes/distributions/native/v0.x/events) — the polling model in full
- [C API reference](/en/runtimes/distributions/native/v0.x/api-reference) — per-function notes
