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:
- C++ Core Engine – Handles model loading, ONNX Runtime sessions, and streaming state management.
- C API Layer – Provides a stable ABI in
core/moonshine-c-api.handcore/moonshine-c-api.cppthat marshals data between C++ objects and C structures. - 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 theMoonshineModelclass, which manages ONNX Runtime inference sessions, vocabulary loading, and audio preprocessing.core/transcriber.cpp– Implements theTranscriberclass 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.hdefines exported symbols, data structures (transcript_t,transcriber_option_t), constants (MOONSHINE_MODEL_ARCH_*), and function prototypes. - Implementation –
core/moonshine-c-api.cppwraps C++ classes (MoonshineModel,Transcriber) and translates between C structures and C++ objects. All public functions are markedMOONSHINE_EXPORTto 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_VERSIONwhen 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 loadslibmoonshine.so,libmoonshine.dylib, ormoonshine.dllviactypes.CDLL. It maps every C function to a ctypes prototype and declares matchingctypes.Structureclasses (TranscriptC,TranscriptLineC,TranscriberOptionC) that mirror the C header field order.transcriber.py– Provides the high-levelTranscriberclass that wraps the low-level calls:loadTranscriberFromFilesreturns a handle (int32).transcribeWithoutStreamingor streaming methods convert Pythonlist[float]to Cfloat*and wrap the returnedTranscriptCinto Python objects.freeTranscriberreleases 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 theMoonshineAPIclass, a singleton that imports C functions directly via the module mapcore/module.modulemap. The module map exposes the C header symbols to Swift asexternfunctions.- Data conversion – Swift uses the imported C structs (
transcript_t,transcript_line_t) as opaque pointers. The wrapper extracts fields, builds Swift structsTranscriptandTranscriptLine, 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
MoonshineErrorif 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 atandroid/java/main/java/ai/moonshine/voice/JNI.java, this class declaresnativestatic methods that correspond one-to-one with the C API (e.g.,moonshineLoadTranscriberFromFiles,moonshineTranscribeWithoutStreaming).moonshine-jni.cpp– The native bridge atandroid/moonshine-jni/moonshine-jni.cppimplements each JNI method. It converts Java strings to C strings, Javafloat[]to Cfloat*, and invokes the C API. It also marshals thetranscript_tresult back into a JavaTranscriptobject (a list ofTranscriptLinePOJOs).- Data objects –
Transcript,TranscriptLine, andTranscriberOptionare 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:
- C++ Core – Implements model loading, ONNX Runtime inference, and streaming logic in
core/moonshine-model.cppandcore/transcriber.cpp. - C API – Exposes a stable ABI through
core/moonshine-c-api.handcore/moonshine-c-api.cpp, guaranteeing backward compatibility via header version checks. - Python Binding – Uses ctypes in
python/src/moonshine_voice/moonshine_api.pyto load the shared library and marshal Python lists to C arrays. - Swift Binding – Imports C symbols directly via
core/module.modulemapinswift/Sources/MoonshineVoice/MoonshineAPI.swift, avoiding JNI overhead. - Java Binding – Bridges JVM calls to the C API through JNI using
android/java/main/java/ai/moonshine/voice/JNI.javaand the native bridgeandroid/moonshine-jni/moonshine-jni.cpp.
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:
- Create a stream handle with
moonshine_create_stream(or language equivalent). - Add audio chunks via
moonshine_add_audio_to_stream. - Transcribe incrementally with
moonshine_transcribe_stream, which returns atranscript_tcontaining flags likeis_completeandis_updated. - Free the stream with
moonshine_free_streamwhen 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →