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.
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.
dotlottie_destroy(player);
free(buffer);Putting both together, teardown runs in the reverse of setup:
/* 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:
{
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:
size_t size = 0;
dotlottie_get_theme_id(player, NULL, &size);
char *theme = malloc(size);
dotlottie_get_theme_id(player, theme, NULL);
/* ... */
free(theme);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.
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);
}
}Buffer sizing
dotlottie_set_sw_target() requires a buffer of exactly width × height 32-bit pixels. There is no stride parameter — rows are contiguous.
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↗.
Checklist
Every
dotlottie_new_player()is matched by adotlottie_destroy()Every
dotlottie_state_machine_load()is matched by adotlottie_state_machine_release(), called firstThe 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 freedEvent 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 — handles, results, and feature gating
Handle events — the polling model in full
C API reference — per-function notes