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

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

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

#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 at lines 62-68.

Object Cleanup

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

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

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

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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →