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

ResourceOwned byReleased by
The player handleThe librarydotlottie_destroy()
A state machine handleThe librarydotlottie_state_machine_release()
Your software render bufferYoufree(), after destroying the player
Strings you pass inYouFree any time after the call returns
Strings you read into your bufferYoufree() when you're done
The marker name from dotlottie_get_marker()The libraryNothing — do not free
String pointers inside polled eventsThe libraryNothing — 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);
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.

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.

PatternSafe
One player used from one threadYes
Separate players on separate threadsYes
One player from several threads, externally lockedYes
One player from several threads, unlockedNo

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 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

Last updated: August 13, 2026 at 9:17 AMEdit this page