How the C API works
Understand the dotLottie C API design - opaque player handles, result codes, feature gating, string conventions, and how it relates to the other players.
The native runtime is not a separate player. It is the C surface of dotlottie-rs, the Rust core that renders every dotLottie animation on every platform. Understanding that relationship explains most of the API's shape.
Where it sits
The Android, iOS, Flutter, and React Native players are thin platform wrappers over this same C API. The web player is the exception — it uses wasm-bindgen bindings to the same core instead.
Two consequences follow. Rendering is identical to every other dotLottie player, because it is literally the same code. And the C API is production-tested rather than a side path — it is what ships in the mobile players.
Opaque handles
The API is handle-based. dotlottie_new_player() returns a DotLottiePlayer *, an opaque pointer whose layout is deliberately not exposed, and nearly every other function takes it as its first argument.
DotLottiePlayer *player = dotlottie_new_player(0);
/* ... */
dotlottie_destroy(player);There are exactly two handle types, and each has its own destructor:
| Handle | Created by | Released by |
DotLottiePlayer * | dotlottie_new_player() | dotlottie_destroy() |
dotlottieDotLottieStateMachine * | dotlottie_state_machine_load() or ..._load_data() | dotlottie_state_machine_release() |
Font loading is the one exception to the handle rule: dotlottie_load_font() and dotlottie_unload_font() take no player, because fonts live in a process-wide registry shared by every player.
Result codes
Fallible functions return dotlottieDotLottieResult. Zero means success, and the header defines DOTLOTTIE_SUCCESS for it.
| Value | Numeric | Meaning |
Success | 0 | The call succeeded |
Error | 1 | A general failure |
InvalidParameter | 2 | A null or out-of-range argument, including an invalid handle |
ManifestNotAvailable | 3 | The loaded file has no manifest |
AnimationNotLoaded | 4 | The operation needs a loaded animation |
InsufficientCondition | 5 | The player is not in a state where the call makes sense |
FeatureNotEnabled | 6 | The library was compiled without the required feature |
Compare against DOTLOTTIE_SUCCESS rather than testing for truthiness, since every failure is a non-zero value:
if (dotlottie_load_animation_path(player, path) != DOTLOTTIE_SUCCESS) {
/* handle the failure */
}Getters that cannot fail
A handful of simple getters return the value directly instead of a result code, and substitute a default when the handle is invalid:
float speed = dotlottie_get_speed(player); /* 1.0 if player is NULL */dotlottie_get_mode(), dotlottie_get_speed(), dotlottie_get_loop(), dotlottie_get_loop_count(), dotlottie_get_autoplay(), dotlottie_get_use_frame_interpolation(), dotlottie_status(), and dotlottie_is_complete() all behave this way.
A speed of 1.0 might mean the animation plays at normal speed, or that you passed a null player. These getters
cannot report an error, so validate your handle separately rather than inferring validity from the value you get back.
Reading strings
The API never allocates memory that you have to free. Instead, functions that return a string use a two-call pattern: ask for the required size, allocate, then ask for the content.
size_t size = 0;
if (dotlottie_get_active_marker(player, NULL, &size) != DOTLOTTIE_SUCCESS) {
return;
}
char *marker = malloc(size); /* size includes the null terminator */
dotlottie_get_active_marker(player, marker, NULL);
/* use marker ... */
free(marker);Passing NULL for the buffer queries the size; passing NULL for size_out skips the size report. Every function with a buffer and size_out pair works this way — manifests, theme IDs, animation IDs, slot IDs and values, and state machine state.
dotlottie_get_marker() is different: it hands back a pointer into memory the library owns, which you must not free.
Feature gating
The runtime is compiled with only the features you ask for, and functions belonging to a feature you left out still exist — they just return FeatureNotEnabled instead of doing anything. This keeps the header and ABI stable no matter how the library was configured.
| Result of | Requires feature |
dotlottie_load_dotlottie_data, dotlottie_get_manifest, dotlottie_load_animation | dotlottie |
dotlottie_set_theme, dotlottie_set_theme_data, dotlottie_get_theme_id | theming |
dotlottie_set_audio_volume, dotlottie_get_audio_volume | audio |
Any dotlottie_state_machine_* function | state-machines |
State machines signal absence differently: dotlottie_state_machine_load() returns NULL when the feature is off, so you will never hold a non-null state machine handle in a build without it.
Getting FeatureNotEnabled from a call you expected to work means rebuilding with that feature. See Choose your features.
Naming in the generated header
The header is generated by cbindgen, which prefixes types but leaves function names as they are. Knowing the rule saves time reading it:
Functions keep their names —
dotlottie_new_player,dotlottie_set_speedTypes gain a
dotlottieprefix —dotlottiePlayer,dotlottieLayout,dotlottieDotLottieResultConvenience aliases are provided for the two you use most —
DotLottiePlayerand theDOTLOTTIE_SUCCESSconstant
Enum values are emitted bare: Success, Error, Play, Pause, Stop, Frame, Render, Loop, Complete,
None, Fill, Cover, Idle, Playing, Surface, Texture. Common words like None, Error, Loop, and
Stop collide easily with application or platform macros. If you hit a conflict, include dotlottie_player.h before
your own headers, or wrap the include in a translation unit that exposes only your own wrapper functions.
Two further quirks in the generated header are worth knowing so you don't chase them:
DotLottieConfigis declared as a typedef to a struct that is never defined. It is a leftover with no use — configure the player through the individualdotlottie_set_*functions instead.Some doc-comment examples inside the header reference constants that don't exist, such as
DotLottiePlayerEventType_Load. The real spelling is the bareLoad.
Error handling across the boundary
The release build compiles Rust with panic = "abort". If the core hits an unrecoverable internal error, the whole process terminates — a panic cannot be caught or converted into a result code by the C caller. In practice this is reserved for genuine bugs; ordinary failures such as a bad path or malformed animation come back as result codes.
Learn more
Memory and safety rules — ownership, lifetimes, and thread safety
Choose a render target — software, OpenGL, and WebGPU
C API reference — every function and type