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

```mermaid
flowchart TD
  A[dotLottie iOS] --> CAPI[C API]
  B[dotLottie Android] --> CAPI
  C[dotLottie Flutter] --> CAPI
  D[dotLottie React Native] --> CAPI
  E[Your C / C++ application] --> CAPI
  F[dotLottie Web] --> WASM[wasm-bindgen]

  CAPI --> Core[dotlottie-rs]
  WASM --> Core

  Core --> ThorVG[ThorVG renderer]
```

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.

```c
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:

```c
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:

```c
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.

<Callout type="warning" title="A default value is indistinguishable from a real one">
  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.
</Callout>

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

```c
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](/en/runtimes/distributions/native/v0.x/building#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_speed`
- **Types** gain a `dotlottie` prefix — `dotlottiePlayer`, `dotlottieLayout`, `dotlottieDotLottieResult`
- **Convenience aliases** are provided for the two you use most — `DotLottiePlayer` and the `DOTLOTTIE_SUCCESS` constant

<Callout type="warning" title="Enum constants are unprefixed and share the global namespace">
  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.
</Callout>

Two further quirks in the generated header are worth knowing so you don't chase them:

- `DotLottieConfig` is declared as a typedef to a struct that is never defined. It is a leftover with no use — configure the player through the individual `dotlottie_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 bare `Load`.

## 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](/en/runtimes/distributions/native/v0.x/memory-and-safety) — ownership, lifetimes, and thread safety
- [Choose a render target](/en/runtimes/distributions/native/v0.x/render-targets) — software, OpenGL, and WebGPU
- [C API reference](/en/runtimes/distributions/native/v0.x/api-reference) — every function and type
