How Moonshine's C++ Core, C API, and Language Bindings (Python, Swift, Java) Are Structured

Moonshine uses a layered architecture where a modern C++ inference engine exposes a stable C API, which Python, Swift, and Java bindings consume via ctypes, direct module imports, and JNI respectively.

The moonshine-ai/moonshine repository implements a high-performance speech-to-text engine designed for cross-platform deployment. To maximize portability without duplicating logic, the project isolates its ONNX Runtime inference code behind a thin C API layer. This design allows language-specific bindings to interact with the core engine while maintaining a single source of truth for model loading, tokenization, and streaming transcription.

Architecture Overview

The codebase follows a strict three-tier separation:

  1. C++ Core Engine – Handles model loading, ONNX Runtime sessions, and streaming state management.
  2. C API Layer – Provides a stable ABI in core/moonshine-c-api.h and core/moonshine-c-api.cpp that marshals data between C++ objects and C structures.
  3. Language Bindings – Thin adapters in Python, Swift, and Java that convert native types to C-compatible structures and invoke the C API.

This separation ensures that updates to the inference engine never break the public interface consumed by the language bindings.

The C++ Core Engine

Located in the core/ directory, the engine implements the actual speech recognition logic.

  • core/moonshine-model.cpp – Implements the MoonshineModel class, which manages ONNX Runtime inference sessions, vocabulary loading, and audio preprocessing.
  • core/transcriber.cpp – Implements the Transcriber class that orchestrates the streaming pipeline, maintaining internal state for partial results.
  • Threading – The core is thread-safe; multiple threads can create independent transcriber instances, though a single transcriber serializes its internal work queue.

The C++ layer never exposes STL containers or exceptions across the library boundary. Instead, it relies on the C API to translate all data into plain C structs.

The C API Layer

The C API provides the stable binary interface that all language bindings target.

  • Header – core/moonshine-c-api.h defines exported symbols, data structures (transcript_t, transcriber_option_t), constants (MOONSHINE_MODEL_ARCH_*), and function prototypes.
  • Implementation – core/moonshine-c-api.cpp wraps C++ classes (MoonshineModel, Transcriber) and translates between C structures and C++ objects. All public functions are marked MOONSHINE_EXPORT to ensure visibility in the shared library.

Key Functions

  • moonshine_load_transcriber_from_files – Initializes a transcriber from model files on disk.
  • moonshine_transcribe_without_streaming – One-shot transcription of complete audio.
  • moonshine_create_stream, moonshine_add_audio_to_stream, moonshine_transcribe_stream – Streaming API for real-time recognition.
  • moonshine_free_transcriber – Releases model resources.

Design Highlights

  • Version safety – Callers pass MOONSHINE_HEADER_VERSION when loading a transcriber, allowing the library to emulate older behavior if the shared library is newer than the client headers.
  • ABI stability – The C API guarantees that structures and function signatures remain compatible across minor versions, preventing binding breakage.

Language Bindings

Each binding is a thin adapter that marshals data between idiomatic language types and the C API.

Python Binding

Located in python/src/moonshine_voice/, the Python binding uses ctypes to load the shared library dynamically.

  • moonshine_api.py – Defines _MoonshineLib, a singleton that loads libmoonshine.so, libmoonshine.dylib, or moonshine.dll via ctypes.CDLL. It maps every C function to a ctypes prototype and declares matching ctypes.Structure classes (TranscriptC, TranscriptLineC, TranscriberOptionC) that mirror the C header field order.
  • transcriber.py – Provides the high-level Transcriber class that wraps the low-level calls:
    1. loadTranscriberFromFiles returns a handle (int32).
    2. transcribeWithoutStreaming or streaming methods convert Python list[float] to C float* and wrap the returned TranscriptC into Python objects.
    3. freeTranscriber releases the handle.

The binding automatically converts Python lists to C arrays and raises MoonshineError when the C API returns non-zero error codes.

from moonshine_voice import Transcriber, ModelArch

# Load a transcriber for the base model (English)

transcriber = Transcriber(
    model_path="models/base/en",
    model_arch=ModelArch.BASE,
)

# Non-streaming transcription of a float PCM array (16 kHz)

audio = [...]                     # List[float] with values in [-1.0, 1.0]

transcript = transcriber.transcribe(audio, sample_rate=16000)

for line in transcript.lines:
    print(f"{line.start_time:.2f}s → {line.text}")

# Clean up

transcriber.close()

Swift Binding

The Swift package resides in swift/Sources/MoonshineVoice/.

  • MoonshineAPI.swift – Defines the MoonshineAPI class, a singleton that imports C functions directly via the module map core/module.modulemap. The module map exposes the C header symbols to Swift as extern functions.
  • Data conversion – Swift uses the imported C structs (transcript_t, transcript_line_t) as opaque pointers. The wrapper extracts fields, builds Swift structs Transcript and TranscriptLine, and copies audio data into Swift arrays after safety checks.
  • Error handling – Each C call returns an integer error code; the Swift wrapper checks this and throws MoonshineError if non-zero.

The Swift binding bypasses JNI or ctypes overhead by linking directly against the compiled libmoonshine library, providing near-native performance.

import MoonshineVoice

do {
    // Load a tiny-streaming model
    let handle = try MoonshineAPI.shared.loadTranscriberFromFiles(
        path: "models/tiny/en",
        modelArch: .tinyStreaming
    )

    // Create a streaming session
    let stream = try MoonshineAPI.shared.createStream(transcriberHandle: handle, flags: 0)
    try MoonshineAPI.shared.startStream(transcriberHandle: handle, streamHandle: stream)

    // Feed audio chunks (Float array) from microphone
    try MoonshineAPI.shared.addAudioToStream(
        transcriberHandle: handle,
        streamHandle: stream,
        audioData: micChunk,
        sampleRate: 16000,
        flags: 0
    )

    // Get partial transcript
    let transcript = try MoonshineAPI.shared.transcribeStream(
        transcriberHandle: handle,
        streamHandle: stream,
        flags: 0
    )
    print(transcript.lines.map { $0.text }.joined(separator: " "))

    // Clean up
    try MoonshineAPI.shared.stopStream(transcriberHandle: handle, streamHandle: stream)
    try MoonshineAPI.shared.freeStream(transcriberHandle: handle, streamHandle: stream)
    MoonshineAPI.shared.freeTranscriber(handle)
} catch {
    print("Moonshine error: \(error)")
}

Java (Android) Binding

The Android binding uses JNI to bridge Java method calls to the C API.

  • JNI.java – Located at android/java/main/java/ai/moonshine/voice/JNI.java, this class declares native static methods that correspond one-to-one with the C API (e.g., moonshineLoadTranscriberFromFiles, moonshineTranscribeWithoutStreaming).
  • moonshine-jni.cpp – The native bridge at android/moonshine-jni/moonshine-jni.cpp implements each JNI method. It converts Java strings to C strings, Java float[] to C float*, and invokes the C API. It also marshals the transcript_t result back into a Java Transcript object (a list of TranscriptLine POJOs).
  • Data objects – Transcript, TranscriptLine, and TranscriberOption are plain Java POJOs used by the application layer.

The Java code simply forwards arguments to the native layer; all heavy lifting (model inference, VAD, tokenization) stays in the C++ core accessed via the C API.

import ai.moonshine.voice.JNI;
import ai.moonshine.voice.Transcript;

public class Demo {
    public static void main(String[] args) {
        // Load the native library (packaged with the app)
        System.loadLibrary("moonshine");

        // Load a base model from assets
        int transcriber = JNI.moonshineLoadTranscriberFromFiles(
                "/android_asset/models/base/en",
                JNI.MOONSHINE_MODEL_ARCH_BASE,
                null // no extra options
        );

        // Non-streaming transcription
        float[] audio = ...; // 16 kHz PCM floats
        Transcript transcript = JNI.moonshineTranscribeWithoutStreaming(
                transcriber,
                audio,
                audio.length,
                16000,
                0 // flags
        );

        for (var line : transcript.getLines()) {
            System.out.printf("[%.2fs] %s%n", line.getStartTime(), line.getText());
        }

        // Clean up
        JNI.moonshineFreeTranscriber(transcriber);
    }
}

Summary

Moonshine’s multi-language architecture relies on strict separation between the inference engine and language wrappers:

Frequently Asked Questions

How does Moonshine maintain ABI stability across versions?

The C API in core/moonshine-c-api.h uses explicit struct layouts and MOONSHINE_EXPORT visibility macros. Callers pass MOONSHINE_HEADER_VERSION when initializing a transcriber via moonshine_load_transcriber_from_files, allowing the library to emulate older behavior if the shared library is newer than the client headers. This ensures that Python, Swift, and Java bindings compiled against an older SDK continue to work with newer core releases.

Why does Moonshine use a C API instead of exposing C++ directly?

A C API provides a stable binary interface that is not affected by C++ ABI changes, name mangling, or STL container differences across compilers. This allows the same compiled libmoonshine.so (or .dylib/.dll) to be consumed by Python via ctypes, Swift via direct symbol import, and Java via JNI without recompilation or per-language C++ wrappers.

Can I use the C API directly without the language bindings?

Yes. The C API is fully public and documented in core/moonshine-c-api.h. You can link against libmoonshine directly from C or C++ code, or from any language that supports C FFI (such as Rust, Go, or Node.js N-API). The language bindings provided in the repository are convenience wrappers that handle memory management and type conversion for their respective ecosystems.

How does streaming transcription work across the different bindings?

All bindings expose the same streaming lifecycle defined in the C API:

  1. Create a stream handle with moonshine_create_stream (or language equivalent).
  2. Add audio chunks via moonshine_add_audio_to_stream.
  3. Transcribe incrementally with moonshine_transcribe_stream, which returns a transcript_t containing flags like is_complete and is_updated.
  4. Free the stream with moonshine_free_stream when done.

The Python, Swift, and Java wrappers map these steps to idiomatic patterns—Python context managers, Swift structured concurrency, or Java try-with-resources—while internally calling the same C functions.

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 →