Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: Rust

on:
push:
branches: [main]
pull_request:

permissions:
contents: read

jobs:
cpu:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
env:
CARGO_BUILD_JOBS: 4
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install stable Rust and checks
run: rustup toolchain install stable --profile minimal --component rustfmt --component clippy
- name: Format
run: cargo +stable fmt --manifest-path rust/Cargo.toml -- --check
- name: Lint
run: cargo +stable clippy --manifest-path rust/Cargo.toml --all-targets -- -D warnings
- name: Real CPU inference and safety tests
run: cargo +stable test --manifest-path rust/Cargo.toml --all-targets
- name: Compile-fail thread-safety contract
run: cargo +stable test --manifest-path rust/Cargo.toml --doc
# This is intentionally a path-only crate; inventory is not a package build.
- name: Inspect non-publishable package inventory
run: cargo +stable package --manifest-path rust/Cargo.toml --list --allow-dirty
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,15 @@ NumPy waveform input and output, audio decoding/resampling, and model lifetime
management. It builds a self-contained CPU extension; no PyTorch or GPU runtime
is needed for inference. See the package README for API examples and tests.

## Rust bindings (CPU)

The [`rust/` crate](rust/README.md) provides safe model ownership, text/audio
embeddings, frame/features APIs, decoding, resampling and log-mel preprocessing.
Use it as a local path dependency from an initialized recursive checkout; it is
intentionally not publishable until native-source vendoring is implemented.
Run `cargo test --manifest-path rust/Cargo.toml -j 4` (Python 3 is needed only to
generate tiny test fixtures). Inference requires no Python or GPU runtime.

## Build and test

```sh
Expand Down
1 change: 1 addition & 0 deletions rust/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/target/
7 changes: 7 additions & 0 deletions rust/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 14 additions & 0 deletions rust/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[package]
name = "clap-cpp"
version = "0.1.0"
edition = "2021"
description = "Safe CPU Rust bindings for clap.cpp"
license = "Apache-2.0"
repository = "https://github.com/gorse-io/clap.cpp"
readme = "README.md"
# Native sources are in the parent checkout and Git submodules, not this crate.
publish = false

[lib]
name = "clap_cpp"
path = "src/lib.rs"
109 changes: 109 additions & 0 deletions rust/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# Rust bindings

`clap-cpp` provides safe, owning CPU bindings to the current C++ `clap::Model`
API. It does **not** depend on the proposed public C API or a system installation
of clap.cpp. Rust has no third-party crate dependencies.

## Build and use

Requirements: Rust stable, CMake >= 3.20, a C/C++17 compiler, and a complete
repository checkout with these pinned submodules initialized:

```sh
git submodule update --init --recursive ggml thirdparty/json thirdparty/icu thirdparty/miniaudio
cargo test --manifest-path rust/Cargo.toml -j 4
cargo run --manifest-path rust/Cargo.toml --example embed -- model.gguf "a piano" audio.wav
```

Python 3 (stdlib only) is needed **only for integration tests**, which generate
tiny valid GGUF models and WAV data without downloading weights. Windows uses
the MSVC Rust toolchain and Visual Studio C++ build tools; macOS uses the Xcode
command-line tools. Native cross-compilation is not currently supported. Choose
moderate Cargo job counts to bound CMake compilation parallelism.

In another local project:

```toml
[dependencies]
clap-cpp = { path = "/path/to/clap.cpp/rust" }
```

```rust,no_run
use clap_cpp::{Model, decode_audio};
# fn main() -> clap_cpp::Result<()> {
let mut model = Model::new("model.gguf", 4)?;
let tokens = model.tokenize("a piano", 512)?;
let embedding = model.text_tokens(&tokens)?;
let audio = decode_audio("piano.wav")?;
let embedding = model.audio(&audio.samples, audio.sample_rate, 0)?;
# Ok(())
# }
```

The independent CMake entry point builds only core, CPU ggml, bundled ICU/json,
and the private bridge. Libraries are static and PIC; GPU backends, host-native
ISA tuning, BLAS/Accelerate, OpenMP, CLI tools and native tests are disabled.
System C/C++ runtimes are still required; no clap/ggml/ICU shared libraries or
ICU data files are needed. CMake's usual `CC`, `CXX`, `CFLAGS`, `CXXFLAGS`,
`CMAKE_GENERATOR` and `CMAKE_TOOLCHAIN_FILE` settings can configure native builds.

## API and safety

- `Model::new(path, threads)` loads one GGUF; `Drop` releases it. Zero threads
selects the native default. Negative thread counts are rejected.
- Models are deliberately **neither `Send` nor `Sync`**. Inference takes
`&mut self`; do not bypass this with unsafe code. Backend same-instance
concurrency is not guaranteed.
- `tokenize`, `text`, `text_tokens`, `audio`, `audio_frames`,
`clamp3_features`, `audio_sample_rate`, `projection_dimension` mirror native
operations. Unsupported architecture/operation combinations return errors.
`tokenize` truncates to a maximum, but does **not** pad. String inference uses
the native architecture's context policy. Empty text can be valid; empty
token slices are not.
- Frame results are time-major `AudioFrames { values, frames, dimension }`.
`values.len() == frames * dimension`. MERT final frames and all-layer CLaMP3
preprocessing features are different operations; CLAP does not expose either.
- `decode_audio` supports WAV/MP3/FLAC and averages channels, preserving source
rate. `resample_audio` uses the native causal linear/anti-alias resampler;
its output is not zero-phase sinc. `log_mel` accepts 48 kHz mono PCM and
returns flattened `[1001, 64]` features with native repeat-padding/cropping.
`audio` performs model-specific resampling; crop offsets retain the native
model's meaning (CLAP: samples after resampling to 48 kHz). Other architectures
may reject nonzero crop offsets.
- Native exceptions are caught inside the private bridge. Every result has a
paired C++ free; output vectors and diagnostics are copied into Rust-owned
values before release, never borrowed from a destroyed native object. An
allocation failure while creating a native result becomes an `Error`; native
diagnostic text is bounded to 1023 bytes. Rust allocation failure retains
Rust's standard process-abort behavior.
- Strings and paths must be UTF-8 and cannot contain NUL. Non-UTF-8 OS paths
are rejected rather than lossy-converted. Input slices are borrowed only for
synchronous calls. PCM must be nonempty and finite with positive sample rate;
invalid tokens and architecture/context constraints are checked natively.

## Packaging limitation (intentional)

This is a **repository/path dependency**, not a standalone crates.io package.
`publish = false` prevents publishing an unusable source archive. Cargo packages
only files inside `rust/`; the parent C++ sources and Git submodules are therefore
absent from `cargo package` archives. A package archive cannot build standalone,
and a plain Cargo Git dependency does not reliably initialize the required
submodules. Use an explicitly cloned, initialized checkout and a path dependency.
Do not remove the publishing guard without first adding a reviewed native-source
vendoring/release process, all dependency licenses, and an isolated package-build
test. `cargo package --list --allow-dirty` can inspect the archive inventory;
that is not evidence of standalone package viability.

## Checks

```sh
cargo fmt --manifest-path rust/Cargo.toml -- --check
cargo clippy --manifest-path rust/Cargo.toml --all-targets -- -D warnings
cargo test --manifest-path rust/Cargo.toml
```

Tests exercise actual CLAP text/waveform and MERT inference, independent scalar
text values, dimensions, frame mean-pooling, decoding/resampling/mel values,
UTF-8 paths, owned errors, unsupported operations, invalid IDs and nonfinite PCM.
Compile-fail doctests enforce the !Send/!Sync contract. Large published models,
GPU inference and exhaustive numerical parity are outside this binding suite.
108 changes: 108 additions & 0 deletions rust/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
use std::{env, path::PathBuf, process::Command};

fn run(command: &mut Command) {
let status = command
.status()
.expect("cannot run CMake; install CMake >=3.20 and a C++17 compiler");
assert!(status.success(), "native CMake build failed: {command:?}");
}

fn main() {
let manifest = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap());
let root = manifest.parent().unwrap();
assert!(root.join("include/clap.h").is_file(),
"clap-cpp requires the parent repository native sources; use a path dependency from a recursive Git checkout, not a cargo package archive (see rust/README.md)");
for path in [
"rust/native",
"src",
"include",
"cmake",
"ggml",
"thirdparty/json",
"thirdparty/icu",
"thirdparty/miniaudio",
] {
println!("cargo:rerun-if-changed={}", root.join(path).display());
}
for var in [
"CMAKE_GENERATOR",
"CMAKE_TOOLCHAIN_FILE",
"CC",
"CXX",
"CFLAGS",
"CXXFLAGS",
] {
println!("cargo:rerun-if-env-changed={var}");
}
let target = env::var("TARGET").unwrap();
assert_eq!(
env::var("HOST").unwrap(),
target,
"cross-compilation is not supported yet"
);
let out = PathBuf::from(env::var_os("OUT_DIR").unwrap());
let build = out.join("build");
let install = out.join("native");
let mut configure = Command::new("cmake");
configure
.arg("-S")
.arg(manifest.join("native"))
.arg("-B")
.arg(&build)
.arg("-DCMAKE_BUILD_TYPE=Release")
.arg(format!("-DCMAKE_INSTALL_PREFIX={}", install.display()));
if let Some(toolchain) = env::var_os("CMAKE_TOOLCHAIN_FILE") {
configure.arg(format!(
"-DCMAKE_TOOLCHAIN_FILE={}",
toolchain.to_string_lossy()
));
}
run(&mut configure);
run(Command::new("cmake")
.arg("--build")
.arg(&build)
.args([
"--config",
"Release",
"--target",
"clap-rust-native",
"--parallel",
])
.arg(env::var("NUM_JOBS").unwrap_or_else(|_| "2".into())));
run(Command::new("cmake").arg("--install").arg(&build).args([
"--config",
"Release",
"--component",
"rust",
]));
println!(
"cargo:rustc-link-search=native={}",
install.join("lib").display()
);
for lib in [
"clap-rust-native",
"clap",
"ggml",
"ggml-cpu",
"ggml-base",
"clap-icu-i18n",
"clap-icu-uc",
"clap-icu-data",
] {
println!("cargo:rustc-link-lib=static={lib}");
}
if target.contains("apple") {
println!("cargo:rustc-link-lib=c++");
} else if target.contains("windows") {
assert!(
target.contains("msvc"),
"Windows requires the MSVC Rust toolchain"
);
// Static ggml CPU detection and ICU timezone support use registry APIs.
println!("cargo:rustc-link-lib=advapi32");
} else {
for lib in ["stdc++", "pthread", "m", "dl"] {
println!("cargo:rustc-link-lib={lib}");
}
}
}
20 changes: 20 additions & 0 deletions rust/examples/embed.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
use clap_cpp::{decode_audio, Model};

fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut args = std::env::args().skip(1);
let path = args
.next()
.ok_or("usage: cargo run --example embed -- MODEL.gguf TEXT [AUDIO]")?;
let text = args.next().ok_or("missing text")?;
let mut model = Model::new(path, 0)?;
println!("projection dimension: {}", model.projection_dimension()?);
println!("text: {:?}", model.text(&text)?);
if let Some(path) = args.next() {
let audio = decode_audio(path)?;
println!(
"audio: {:?}",
model.audio(&audio.samples, audio.sample_rate, 0)?
);
}
Ok(())
}
39 changes: 39 additions & 0 deletions rust/native/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
cmake_minimum_required(VERSION 3.20)
project(clap_cpp_rust VERSION 0.1.0 LANGUAGES C CXX)

get_filename_component(CLAP_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../.." ABSOLUTE)
foreach(source ggml/CMakeLists.txt thirdparty/json/CMakeLists.txt thirdparty/miniaudio/miniaudio.h)
if(NOT EXISTS "${CLAP_ROOT}/${source}")
message(FATAL_ERROR "Native sources missing: run git submodule update --init --recursive")
endif()
endforeach()

# Build a self-contained CPU extension, never CLI executables, GPU backends,
# external OpenMP runtimes, or shared libraries requiring a loader search path.
set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE)
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
set(GGML_NATIVE OFF CACHE BOOL "" FORCE)
set(GGML_CPU ON CACHE BOOL "" FORCE)
foreach(backend METAL CUDA HIP VULKAN SYCL OPENCL OPENVINO RPC MUSA WEBGPU ZDNN
VIRTGPU VIRTGPU_BACKEND ET HEXAGON ZENDNN BLAS ACCELERATE)
set(GGML_${backend} OFF CACHE BOOL "" FORCE)
endforeach()
foreach(feature BACKEND_DL OPENMP BUILD_TESTS BUILD_EXAMPLES)
set(GGML_${feature} OFF CACHE BOOL "" FORCE)
endforeach()
# Avoid host-specific x86 ISA requirements in downstream executables.
foreach(isa SSE42 AVX AVX2 AVX_VNNI BMI2 FMA F16C AVX512 AVX512_VBMI
AVX512_VNNI AVX512_BF16 AMX_TILE AMX_INT8 AMX_BF16)
set(GGML_${isa} OFF CACHE BOOL "" FORCE)
endforeach()
set(CLAP_USE_SYSTEM_ICU OFF CACHE BOOL "" FORCE)
add_subdirectory("${CLAP_ROOT}/ggml" ggml EXCLUDE_FROM_ALL)
add_subdirectory("${CLAP_ROOT}/thirdparty/json" json EXCLUDE_FROM_ALL)
include("${CLAP_ROOT}/cmake/icu.cmake")
add_subdirectory("${CLAP_ROOT}/src" core EXCLUDE_FROM_ALL)

add_library(clap-rust-native STATIC bridge.cpp)
target_link_libraries(clap-rust-native PRIVATE clap)
target_compile_features(clap-rust-native PRIVATE cxx_std_17)
install(TARGETS clap-rust-native clap ggml ggml-base ggml-cpu clap-icu-i18n clap-icu-uc clap-icu-data
ARCHIVE DESTINATION lib COMPONENT rust)
Loading
Loading