Home/Documentation/Integrate CK

Integrate CK

Call a CK kernel from Node.js, Python, or Rust.

CK is designed to handle a focused calculation inside a larger application. Keep your application in Node.js, Python, or Rust, and move a measured, compute-heavy routine into a CK function when a small boundary makes sense.

Why not write everything in Rust or C++?#

Rust and C++ are strong choices for complete applications, system software, and code that depends on their mature libraries and tooling. CK has a narrower role: describe a calculation, compile it for a supported target, and expose it for another program to call.

You do not need to rewrite an application to try CK. Keep its routes, files, database, and user interface where they are. Try moving one calculation across the boundary, then measure the real workload. CK is not automatically faster than Rust or C++; the result depends on the algorithm, data, and target machine.

When is CK worth trying?#

CK is a good candidate when profiling shows that a small numerical routine takes a meaningful share of the time, and the routine can receive its inputs and return its result through a clear function interface. Examples include repeated scoring, numeric transforms, and parts of simulations or image processing.

Keep the calculation in its current language when it is already fast enough, spends most of its time waiting on files or network requests, or relies on a library that already performs the work efficiently. Start with one function and compare the complete application path, including the cost of calling across the boundary.

Make one function callable#

Mark a top-level function with export. This example takes two signed 64-bit integers and returns their sum:

export fn add_i64(a: i64, b: i64) -> i64 {
  return a + b;
}

Save it as kernel.ck and check it:

ckc check kernel.ck

The export keyword makes the function available to a supported output target. It does not choose how your application will load it. Pick the boundary that fits your host:

Host project Starting point What to know
Node.js WebAssembly A scalar export can be called from Node's built-in WebAssembly API.
Python Native shared library and ctypes This is a possible C ABI route; the repository does not currently test a Python binding.
Rust Native shared library or static library and C FFI CK emits a C ABI header; the repository has internal library-call coverage, but no standalone Cargo example.

Node.js: call a scalar WebAssembly export#

Emit a WebAssembly module:

ckc emit-wasm kernel.ck --out kernel.wasm

In a Node.js project, save this as host.mjs next to kernel.wasm:

import { readFile } from 'node:fs/promises'

const bytes = await readFile(new URL('./kernel.wasm', import.meta.url))
const { instance } = await WebAssembly.instantiate(bytes)

console.log(instance.exports.add_i64(20n, 22n).toString())

Run it with node host.mjs; it prints 42. WebAssembly i64 values cross the JavaScript boundary as BigInt, which is why the inputs use 20n and 22n.

This scalar example uses a tested Node/WebAssembly export shape. For pointers or slices, the host must allocate and manage WebAssembly memory and pass byte addresses; CK does not provide an allocator. WebAssembly output currently supports unchecked overflow and bounds only. See outputs and backends before exposing memory-heavy functions.

Python: use the Native C ABI through ctypes#

A Python application can load a Native dynamic library with Python's standard ctypes module. These Native examples need a release ckc with Native support, available from the CalcKernel release page. Build the library on the machine where it will run, using a CK file that contains the add_i64 function above:

ckc build kernel.ck --kind dynamic --out libkernel

The compiler writes a platform-specific library and a generated C header beside it. With the output name above, the library is libkernel.dylib on macOS, libkernel.so on Linux, and kernel.dll on Windows; the generated header is libkernel.h. Save this as host.py in the same folder and run python host.py there:

import ctypes
import sys
from pathlib import Path

if sys.platform == "darwin":
    library_name = "libkernel.dylib"
elif sys.platform == "win32":
    library_name = "kernel.dll"
else:
    library_name = "libkernel.so"

library_path = Path(__file__).resolve().parent / library_name
library = ctypes.CDLL(str(library_path))

add_i64 = library.add_i64
add_i64.argtypes = [ctypes.c_int64, ctypes.c_int64]
add_i64.restype = ctypes.c_int64

print(add_i64(20, 22))

It prints 42. The ctypes types match the generated header's int64_t parameters and return value. If you use a different output name or folder, update the library path to the generated file.

This is an integration path, not a first-party Python binding: the repository does not currently ship a Python package or a tested ctypes wrapper. Begin with scalar functions and verify the load path, symbol, and types on each platform you support. CK does not manage buffers passed by the host; the Python side must keep any memory alive and valid for the entire call. Read memory and safety before passing pointers or slices.

Rust: call the generated C ABI#

Rust can link a Native static library and declare its exported functions at an extern "C" boundary. Build it on the same machine and target as the Rust program, using a CK file that contains the add_i64 function above:

ckc build kernel.ck --kind static --out libkernel_static

This creates libkernel_static.a and libkernel_static.h on macOS and Linux. On Windows, the library is kernel_static.lib and the header remains libkernel_static.h. Save the following as host.rs in the same folder:

#[link(name = "kernel_static", kind = "static")]
unsafe extern "C" {
    fn add_i64(a: i64, b: i64) -> i64;
}

fn main() {
    let answer = unsafe { add_i64(20, 22) };
    println!("{answer}");
}

Compile and run it with:

rustc host.rs -L native=. -o host
./host

It prints 42. The generated libkernel_static.h is the authority for exported names, C types, and calling details. Rust declarations must match it exactly; calls through a raw foreign-function boundary are unsafe. On Windows, link kernel_static.lib with a matching Rust target and linker, and use .exe for the program. The repository verifies Native library symbol loading internally, but does not provide a ready-made Cargo crate. This static example avoids dynamic-library runtime search paths, which vary by platform.

For a first connection, keep the interface to scalar inputs and outputs. If you pass arrays or pointers, Rust must provide correctly sized, aligned, live memory for the duration of the call. See Native and C ABI details and memory and safety.

Keep the first boundary small#

Start with one exported function and simple values. Check the .ck file, build or emit the target you chose, and call it from a small host program. Then measure with representative inputs and compare against the same operation in the existing application.

Native and C library headers define the actual foreign-function interface. In the default unchecked mode, simple scalar functions use direct C argument and return types. If you enable checked overflow or bounds, the generated Native ABI can include a status result and output parameter; read the generated header rather than assuming the unchecked signature. For pointer and slice rules, read memory and safety.

Repository reference links follow the main branch and may describe features newer than the latest downloadable release.

↵ open · esc close