# Moonshine C API Handle-Based Object Management for Cross-Language Compatibility

> Learn how Moonshine's C API uses handle-based object management for cross-language compatibility. Discover thread-safe maps and lifetime management for seamless integration.

- Repository: [Moonshine AI/moonshine](https://github.com/moonshine-ai/moonshine)
- Tags: internals
- Published: 2026-02-16

---

**Moonshine's C API uses opaque `int32_t` integer handles to represent C++ objects, enabling safe cross-language interoperability through thread-safe internal maps that validate and manage object lifetimes.**

Moonshine is an open-source speech recognition toolkit that provides a C API for cross-language compatibility. The Moonshine C API handle-based object management system abstracts complex C++ objects—such as transcribers, streams, and intent recognizers—behind simple integer handles, allowing developers to integrate Moonshine into Python, Rust, Java, and other environments without exposing C++ internals.

## The Handle-Based Architecture

The architecture centers on opaque integer handles that act as proxies for C++ instances stored in internal maps.

### Opaque Integer Handles

The API exposes only `int32_t` values to callers. These handles are generated monotonically and represent complex objects like `Transcriber` or `IntentRecognizer` instances. Because handles are plain integers, any language capable of calling C functions can store and pass them without understanding C++ object layouts.

### Internal Object Mapping

Inside [`core/moonshine-c-api.cpp`](https://github.com/moonshine-ai/moonshine/blob/main/core/moonshine-c-api.cpp), the library maintains `std::map<int32_t, Transcriber*>` and `std::map<int32_t, IntentRecognizer*>` structures. Each map entry associates a handle with its underlying C++ object pointer. All map operations are protected by `std::mutex` instances (`transcriber_map_mutex` and `intent_recognizer_map_mutex`) to ensure thread safety.

## Cross-Language Compatibility Benefits

The handle-based design delivers specific advantages for multi-language integration.

### Language Agnostic Interface

By restricting the public interface to C functions and `int32_t` handles, the API eliminates C++ ABI dependencies. Functions like `moonshine_load_transcriber_from_files` and `moonshine_create_stream` accept and return only integer handles, making them callable from Python via `ctypes`, Rust via `ffi`, or Java via JNI without marshaling complex structures.

### Automatic Lifetime Management

The API retains ownership of all C++ objects. Callers receive handles but never raw pointers. When finished, callers invoke `moonshine_free_transcriber` or `moonshine_free_intent_recognizer`, which trigger internal `free_*_handle` helpers. These helpers delete the C++ object and erase the map entry, preventing memory leaks and use-after-free errors.

### Thread-Safe Handle Operations

Concurrent callers can safely create, use, and destroy handles. The mutex-protected maps in [`core/moonshine-c-api.cpp`](https://github.com/moonshine-ai/moonshine/blob/main/core/moonshine-c-api.cpp) ensure that allocation, lookup, and deallocation operations are atomic. This allows multi-threaded applications in languages like Python or Rust to share transcriber instances across threads without additional synchronization.

## Implementation Details in Moonshine

The handle system relies on specific implementation patterns for allocation, validation, and cleanup.

### Handle Allocation and Generation

Each object type maintains a monotonically increasing counter. The `allocate_transcriber_handle` function locks the mutex, increments `next_transcriber_handle`, and stores the pointer in `transcriber_map`.

```cpp
int32_t allocate_transcriber_handle(Transcriber *transcriber) {
    std::lock_guard<std::mutex> lock(transcriber_map_mutex);
    int32_t handle = next_transcriber_handle++;
    transcriber_map[handle] = transcriber;
    return handle;
}

```

This pattern appears in [`core/moonshine-c-api.cpp`](https://github.com/moonshine-ai/moonshine/blob/main/core/moonshine-c-api.cpp) at lines 17-22.

### Handle Validation Macros

Every public API function validates handles before dereferencing them. The `CHECK_TRANSCRIBER_HANDLE` macro verifies that the handle exists in the map and returns `MOONSHINE_ERROR_INVALID_HANDLE` if not.

```cpp
#define CHECK_TRANSCRIBER_HANDLE(handle)                      \
  do {                                                       \
    if (handle < 0 || !transcriber_map.contains(handle)) {   \
      LOGF("Moonshine transcriber handle is invalid: %d",  \
            handle);                                        \
      return MOONSHINE_ERROR_INVALID_HANDLE;                \
    }                                                        \
  } while (0)

```

This macro is defined in [`core/moonshine-c-api.cpp`](https://github.com/moonshine-ai/moonshine/blob/main/core/moonshine-c-api.cpp) at lines 62-68.

### Object Cleanup

The `free_transcriber_handle` helper acquires the lock, deletes the C++ object, and removes the map entry.

```cpp
void free_transcriber_handle(int32_t handle) {
    std::lock_guard<std::mutex> lock(transcriber_map_mutex);
    delete transcriber_map[handle];
    transcriber_map.erase(handle);
}

```

This implementation appears in [`core/moonshine-c-api.cpp`](https://github.com/moonshine-ai/moonshine/blob/main/core/moonshine-c-api.cpp) at lines 24-29.

## Practical Code Examples

### Basic Transcriber Lifecycle in C

The following example demonstrates loading a transcriber, creating a stream, processing audio, and cleanup using integer handles.

```c
#include "moonshine-c-api.h"

int main(void) {
    // Load a transcriber from files – returns a handle (>0) or an error (<0)
    int32_t transcriber = moonshine_load_transcriber_from_files(
        "models/base", MOONSHINE_MODEL_ARCH_BASE, NULL, 0,
        MOONSHINE_HEADER_VERSION);
    if (transcriber < 0) {
        fprintf(stderr, "Error: %s\n", moonshine_error_to_string(transcriber));
        return 1;
    }

    // Create a stream for real-time audio
    int32_t stream = moonshine_create_stream(transcriber, 0);
    moonshine_start_stream(transcriber, stream);

    // ... feed audio chunks ...
    moonshine_transcribe_add_audio_to_stream(transcriber, stream,
                                            audio_chunk, chunk_len, 16000, 0);

    // Pull a transcript
    struct transcript_t *out = NULL;
    moonshine_transcribe_stream(transcriber, stream, 0, &out);
    printf("%s\n", moonshine_transcript_to_string(out));

    // Clean up
    moonshine_stop_stream(transcriber, stream);
    moonshine_free_stream(transcriber, stream);
    moonshine_free_transcriber(transcriber);
    return 0;
}

```

### Python Integration via ctypes

Python can interact with Moonshine handles using the `ctypes` module, treating the `int32_t` handles as standard Python integers.

```python
import ctypes, pathlib

lib = ctypes.CDLL('libmoonshine.so')

# Function signatures

lib.moonshine_load_transcriber_from_files.argtypes = [
    ctypes.c_char_p, ctypes.c_uint32,
    ctypes.c_void_p, ctypes.c_uint64, ctypes.c_int32]
lib.moonshine_load_transcriber_from_files.restype = ctypes.c_int32

lib.moonshine_transcribe_without_streaming.argtypes = [
    ctypes.c_int32, ctypes.POINTER(ctypes.c_float),
    ctypes.c_uint64, ctypes.c_int32, ctypes.c_uint32,
    ctypes.POINTER(ctypes.c_void_p)]
lib.moonshine_transcribe_without_streaming.restype = ctypes.c_int32

# Load model

handle = lib.moonshine_load_transcriber_from_files(
    b'models/base', 1, None, 0, 20000)
if handle < 0:
    raise RuntimeError('Failed to load transcriber')

# Prepare dummy audio (e.g., 1 second of silence)

samples = (ctypes.c_float * 16000)()
out_ptr = ctypes.c_void_p()
err = lib.moonshine_transcribe_without_streaming(
    handle, samples, 16000, 16000, 0, ctypes.byref(out_ptr))
if err != 0:
    raise RuntimeError('Transcription error')

# Cleanup

lib.moonshine_free_transcriber(handle)

```

## Summary

Moonshine's C API implements a robust handle-based object management system that enables seamless cross-language compatibility. Key takeaways include:

- **Opaque integer handles** (`int32_t`) abstract C++ objects, allowing any language with C interop to use the API without exposing C++ internals.
- **Thread-safe internal maps** protected by mutexes ensure concurrent access to transcribers and intent recognizers is safe across multiple threads.
- **Automatic lifetime management** via `moonshine_free_transcriber` and similar functions prevents memory leaks while keeping object ownership inside the library.
- **Validation macros** like `CHECK_TRANSCRIBER_HANDLE` provide early error detection, returning `MOONSHINE_ERROR_INVALID_HANDLE` for stale or invalid references.
- **Extensible design** allows new object types to follow the same pattern using dedicated maps and handle generators, as demonstrated by the intent recognizer subsystem.

## Frequently Asked Questions

### How does Moonshine prevent memory leaks when using handles from other languages?

The API maintains full ownership of C++ objects internally. When a caller creates a transcriber or intent recognizer, the library allocates the object and stores it in a protected map, returning only an integer handle. To release resources, the caller must explicitly invoke `moonshine_free_transcriber` or `moonshine_free_intent_recognizer`, which trigger internal `free_*_handle` helpers. These helpers delete the C++ object and erase the map entry, preventing memory leaks even when the calling language lacks deterministic destructors.

### Can multiple threads safely use the same transcriber handle simultaneously?

Yes, the handle-based architecture is thread-safe by design. All internal maps (`transcriber_map`, `intent_recognizer_map`) are protected by `std::mutex` instances (`transcriber_map_mutex`, `intent_recognizer_map_mutex`). Every operation—whether allocating a new handle, looking up an existing one, or freeing resources—acquires a lock guard before accessing the map. This ensures that concurrent calls from multi-threaded Python applications or Rust programs cannot corrupt the internal state or cause race conditions during object destruction.

### What happens if I pass an invalid or stale handle to a Moonshine function?

The API validates every handle before dereferencing it. Each public function begins with a validation macro such as `CHECK_TRANSCRIBER_HANDLE` or `CHECK_INTENT_RECOGNIZER_HANDLE`. These macros verify that the handle is non-negative and exists in the corresponding map. If validation fails, the function immediately returns `MOONSHINE_ERROR_INVALID_HANDLE` and logs the error, preventing segmentation faults or undefined behavior that would occur if a stale pointer were dereferenced. This defensive programming makes the API robust against programming errors in client code.

### Is the handle pattern extensible for new object types in Moonshine?

Yes, the architecture follows a consistent template that can be replicated for new components. The intent recognizer subsystem demonstrates this extensibility: it uses a separate `intent_recognizer_map` and `intent_recognizer_map_mutex`, along with its own allocation counter (`next_intent_recognizer_handle`) and validation macro (`CHECK_INTENT_RECOGNIZER_HANDLE`). Developers adding new object types can follow this same pattern—creating a dedicated map, mutex, and handle generator—to maintain thread safety and API consistency without modifying existing handle logic.