Your first program
This tutorial has six parts: four build your first ClikaRT program, one serves it, and one deploys it to mobile. Each part is a complete program that compiles and runs on any machine the runtime supports (no accelerator is needed). Core concepts are explained where they first appear. This part links the bundle, prints the version, and makes one tensor.
The C++, C, Kotlin, Go and Rust paths assume the bundle is extracted and CLIKART_BUNDLE_DIR points at it (see Quick install); the Python wheel carries the runtime itself.
The program
- C++
- C
- Python
- Kotlin
- Go
- Rust
#include <cstdio>
#include <ClikaRT/clika_rt.h>
using ClikaRT::DataType;
using ClikaRT::Tensor;
namespace ops = ClikaRT::ops;
int main() {
std::printf("ClikaRT %s\n", ClikaRT::GetVersionInfo().c_str());
// A 2x3 tensor of ones on the CPU (the default device).
const Tensor a = Tensor::ones({2, 3}, DataType::Float32);
// ops:: are free functions returning the value directly.
const Tensor b = ops::add(a, a);
std::printf("a + a =\n%s\n", b.to_string().c_str());
try {
const Tensor bad = ops::matmul(a, Tensor::ones({5, 7}, DataType::Float32));
} catch (const ClikaRT::Error& e) {
std::printf("failed: %s\n", e.what()); // e.status() has the coarse category
}
return 0;
}
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>
#include "clika_rt/clika_rt_core.h"
/* The api-table bootstrap shared by every C sample: dlopen the runtime,
* handshake at the header's ABI version, exit loudly on any runtime error. */
static const clika_rt_api* api;
static void bootstrap(void) {
void* so = dlopen("libClikaRT.so", RTLD_NOW | RTLD_GLOBAL);
if (so == NULL) { fprintf(stderr, "dlopen: %s\n", dlerror()); exit(1); }
const clika_rt_api* (*get_api)(uint32_t) =
(const clika_rt_api* (*)(uint32_t))dlsym(so, "clika_rt_get_api");
api = get_api != NULL ? get_api(CLIKA_RT_ABI_VERSION) : NULL;
if (api == NULL) { fprintf(stderr, "clika_rt_get_api: handshake failed\n"); exit(1); }
}
static void check(clika_rt_error* e, const char* where) {
if (e == NULL) return;
char msg[512];
size_t len = sizeof msg;
if (api->error_message(e, msg, &len) != CLIKA_RT_STATUS_OK) msg[0] = '\0';
fprintf(stderr, "%s failed: %s\n", where, msg);
api->error_free(e);
exit(1);
}
int main(void) {
bootstrap();
const clika_rt_stream_or_device dflt = {CLIKA_RT_STREAM_OR_DEVICE_DEFAULT};
char ver[128];
size_t vlen = sizeof ver;
api->version_string(ver, &vlen);
printf("ClikaRT %s\n", ver);
/* A 2x3 tensor of ones on the CPU (the default device). */
const int64_t shape[2] = {2, 3};
clika_rt_tensor* a = NULL;
check(api->tensor_ones(shape, 2, CLIKA_RT_DATA_TYPE_FLOAT32, dflt, dflt, &a), "ones");
/* Ops are api-table members returning the value through an out-param.
* op_add_tensor CONSUMES its operand (the donation law) and BORROWS the
* embedded rhs tensor; using a on both sides needs one extra reference,
* or the borrow reads a dead handle. */
api->tensor_retain(a);
api->tensor_retain(a); /* and one more: the failing matmul below donates it too */
clika_rt_scalar_or_tensor rhs = {.kind = CLIKA_RT_SCALAR_OR_TENSOR_TENSOR, .tensor = a};
clika_rt_tensor* b = NULL;
check(api->op_add_tensor(a, rhs, 1.0, CLIKA_RT_ACTIVATION_IDENTITY, &b), "add");
api->tensor_release(a); /* the reference retained above */
char buf[256];
size_t blen = sizeof buf;
api->tensor_to_string(b, buf, &blen);
printf("a + a =\n%s\n", buf);
/* A fallible member returns an error object (NULL = success); on failure the
* handle-out stays NULL. Branch on the code NAME, never the message text. */
clika_rt_tensor* wrong = NULL;
check(api->tensor_ones((const int64_t[]){5, 7}, 2, CLIKA_RT_DATA_TYPE_FLOAT32,
dflt, dflt, &wrong), "ones");
clika_rt_tensor* bad = NULL;
clika_rt_error* e = api->op_matmul(a, wrong, NULL, 0, CLIKA_RT_ACTIVATION_IDENTITY,
0, 0, 0.0, 0.0, &bad);
if (e != NULL) {
char msg[512], code[128];
size_t mlen = sizeof msg, clen = sizeof code;
api->error_message(e, msg, &mlen);
api->error_code_name(e, code, &clen);
printf("failed: %s [code: %s]\n", msg, code); /* error_status(e): coarse category */
api->error_free(e);
}
api->tensor_release(b);
return 0;
}
import clika_runtime as crt
def main() -> None:
print(f"ClikaRT {crt.version()}")
# A 2x3 tensor of ones on the CPU (the default device). Factories take
# the shape directly; float32 is the default dtype.
a = crt.ones(2, 3)
# Operators read as math; every result is a new tensor.
b = a + a
print(f"a + a =\n{b}")
try:
bad = a @ crt.ones(5, 7)
except RuntimeError as e:
print(f"failed: {e}") # the message ends in the machine-readable [code: NAME]
if __name__ == "__main__":
main()
import io.clika.runtime.ClikaRt
import io.clika.runtime.ClikaRtException
import io.clika.runtime.Tensors
import io.clika.runtime.matmul
import io.clika.runtime.plus
import io.clika.runtime.summary
fun main() {
ClikaRt.load()
println("ClikaRT ${ClikaRt.version()}")
// A 2x3 tensor of ones on the CPU (the default device); float32 is
// the default dtype.
val a = Tensors.ones(longArrayOf(2, 3))
// Operator extensions return a fresh tensor and leave their operands
// live; release the tensors you named.
val b = a + a
println("a + a =\n${b.summary()}")
try {
val bad = a matmul Tensors.ones(longArrayOf(5, 7))
} catch (e: ClikaRtException) {
println("failed: ${e.message}") // e.codeName is the machine channel, e.status the category
}
b.release()
a.release()
}
//go:build ignore
package main
import (
"errors"
"fmt"
"log"
"github.com/Clika/clika_runtime/bindings/go/clikart"
)
// must keeps the happy path readable; real programs branch on the error.
func must[T any](v T, err error) T {
if err != nil {
log.Fatal(err)
}
return v
}
func main() {
// Load the runtime and handshake at the package's ABI version.
api := must(clikart.Load("libClikaRT.so"))
fmt.Printf("ClikaRT %s\n", must(api.Version()))
// A 2x3 tensor of ones on the CPU (the default device).
a := must(api.TensorOnes([]int64{2, 3}, clikart.Float32,
clikart.StreamOrDevice{}, clikart.StreamOrDevice{}))
// Ops return the value directly; an operand slot takes a scalar or a
// tensor, and every wrapper is non-consuming (the retain rides inside).
b := must(a.AddTensor(clikart.ScalarOrTensor{
Kind: clikart.ScalarOrTensorTensor, Tensor: a,
}, 1.0, clikart.ActivationIdentity))
fmt.Printf("a + a =\n%s\n", b)
// A fallible call answers (value, error); a runtime failure is a
// *clikart.Error. Branch on the code NAME, never the message text.
wrong := must(api.TensorOnes([]int64{5, 7}, clikart.Float32,
clikart.StreamOrDevice{}, clikart.StreamOrDevice{}))
if _, err := clikart.Matmul(a, wrong); err != nil {
var e *clikart.Error
if errors.As(err, &e) {
fmt.Printf("failed: %s\n", e.Message) // e.CodeName is the machine channel, e.Status the category
}
}
b.Release()
a.Release()
}
use clika_rt::{sys, Api};
const F32: sys::clika_rt_data_type = sys::clika_rt_data_type_CLIKA_RT_DATA_TYPE_FLOAT32;
fn main() -> clika_rt::Result<()> {
// Load the runtime and handshake at the crate's ABI version.
let api = Api::load("libClikaRT.so")?;
println!("ClikaRT {}", api.version()?);
// A 2x3 tensor of ones on the CPU (the default device), through the
// full factory.
let a = api.tensor_full(&[2, 3], 1.0, F32)?;
// Operators borrow their operands and return a fresh tensor. Explicit
// calls (the `f_*` Result forms) consume their tensor args instead;
// pass a `shallow_clone()` when the original stays in play.
let b = &a + &a;
println!("a + a =\n{}", api.tensor_to_string(&b)?);
// f_op_matmul consumes its tensor args; `a.shallow_clone()` keeps `a` live.
let wrong = api.tensor_full(&[5, 7], 1.0, F32)?;
if let Err(e) = api.f_op_matmul(a.shallow_clone(), wrong, api.absent(), false,
sys::clika_rt_activation_CLIKA_RT_ACTIVATION_IDENTITY, false, false, 0.0, 0.0) {
println!("failed: {e}"); // e.code_name is the machine channel, e.status the category
}
Ok(())
}
Set up, build, run
The setup is where the languages differ.
- C++
- C
- Python
- Kotlin
- Go
- Rust
The CMake side is three lines of substance (the package and one target).
cmake_minimum_required(VERSION 3.20)
project(hello_clikart LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(ClikaRT CONFIG REQUIRED)
add_executable(hello main.cpp)
target_link_libraries(hello PRIVATE ClikaRT::ClikaRT)
cmake -S . -B build -DClikaRT_DIR="$CLIKART_BUNDLE_DIR/cmake"
cmake --build build
./build/hello
Nothing links against the runtime at build time; the program loads it at run time, so the build needs only the bundle's C header and dl.
cmake_minimum_required(VERSION 3.20)
project(hello_clikart LANGUAGES C)
set(CMAKE_C_STANDARD 99)
set(CMAKE_C_STANDARD_REQUIRED ON)
add_executable(hello main.c)
target_include_directories(hello PRIVATE "${CLIKART_BUNDLE_DIR}/include")
target_link_libraries(hello PRIVATE ${CMAKE_DL_LIBS})
cmake -S . -B build -DCLIKART_BUNDLE_DIR="$CLIKART_BUNDLE_DIR"
cmake --build build
LD_LIBRARY_PATH="$CLIKART_BUNDLE_DIR/lib" ./build/hello
The wheel carries the runtime and its backends; the bundle is not needed on this path. Get ClikaRT has the wheel to download (on Linux, the flavor follows your NVIDIA driver) and the install command. numpy is the array bridge for building tensors from Python data (optional by design, and these pages use it).
pip install ./clika_runtime-<version>-<cpython>-<platform>.whl
pip install numpy
python main.py
The artifact ships the Kotlin API and its JNI bridge; libClikaRT.so comes from the bundle, so run the JVM with -Djava.library.path pointing at the bundle's lib directory.
import io.clika.runtime.ClikaRt
import io.clika.runtime.ClikaRtException
import io.clika.runtime.Tensors
import io.clika.runtime.matmul
import io.clika.runtime.plus
import io.clika.runtime.summary
fun main() {
ClikaRt.load()
println("ClikaRT ${ClikaRt.version()}")
// A 2x3 tensor of ones on the CPU (the default device); float32 is
// the default dtype.
val a = Tensors.ones(longArrayOf(2, 3))
// Operator extensions return a fresh tensor and leave their operands
// live; release the tensors you named.
val b = a + a
println("a + a =\n${b.summary()}")
try {
val bad = a matmul Tensors.ones(longArrayOf(5, 7))
} catch (e: ClikaRtException) {
println("failed: ${e.message}") // e.codeName is the machine channel, e.status the category
}
b.release()
a.release()
}
./gradlew run
The module wraps the runtime's C ABI through cgo and vendors the header it needs; libClikaRT.so comes from the bundle, found through the system loader at run time.
go mod init hello
go get github.com/Clika/clika_runtime/bindings/go@latest
LD_LIBRARY_PATH="$CLIKART_BUNDLE_DIR/lib" go run .
Api::load opens libClikaRT.so through the system loader; the bundle's lib directory on LD_LIBRARY_PATH is enough.
[dependencies]
clika-rt = "0.1"
LD_LIBRARY_PATH="$CLIKART_BUNDLE_DIR/lib" cargo run
ClikaRT 0.4.6
a + a =
Tensor(shape=[2, 3], dtype=Float32, device=CPU, numel=6, data=[2, 2, 2, 2, 2, 2])
failed: ops::matmul: cannot contract a [2, 3] with b [5, 7]: a's last dim (3) must equal b's dim 0 (5)
The version line proves the runtime loaded, and the tensor prints its shape, dtype, device and values, every element 2. The last line is the failure path, demonstrated at the end of the program and explained below.
One library, one header
The umbrella header ClikaRT/clika_rt.h includes the entire public API (tensors, operators, devices, I/O, the serving runtime). ClikaRT::ClikaRT is the only link target. Nothing else from the bundle enters your build; the accelerator backends are shared libraries the runtime loads on its own at run time (part 3).
Values out, ClikaRT::Error on failure
Every fallible call in the public API (factories like Tensor::ones, operators like ops::add) returns its result directly and raises ClikaRT::Error on failure. There are no output parameters and no error codes to check at each call; a shape mismatch or an unavailable device surfaces as an exception where the call was made:
- C++
- C
- Python
- Kotlin
- Go
- Rust
try {
const Tensor bad = ops::matmul(a, Tensor::ones({5, 7}, DataType::Float32));
} catch (const ClikaRT::Error& e) {
std::printf("failed: %s\n", e.what()); // e.status() has the coarse category
}
/* A fallible member returns an error object (NULL = success); on failure the
* handle-out stays NULL. Branch on the code NAME, never the message text. */
clika_rt_tensor* wrong = NULL;
check(api->tensor_ones((const int64_t[]){5, 7}, 2, CLIKA_RT_DATA_TYPE_FLOAT32,
dflt, dflt, &wrong), "ones");
clika_rt_tensor* bad = NULL;
clika_rt_error* e = api->op_matmul(a, wrong, NULL, 0, CLIKA_RT_ACTIVATION_IDENTITY,
0, 0, 0.0, 0.0, &bad);
if (e != NULL) {
char msg[512], code[128];
size_t mlen = sizeof msg, clen = sizeof code;
api->error_message(e, msg, &mlen);
api->error_code_name(e, code, &clen);
printf("failed: %s [code: %s]\n", msg, code); /* error_status(e): coarse category */
api->error_free(e);
}
try:
bad = a @ crt.ones(5, 7)
except RuntimeError as e:
print(f"failed: {e}") # the message ends in the machine-readable [code: NAME]
try {
val bad = a matmul Tensors.ones(longArrayOf(5, 7))
} catch (e: ClikaRtException) {
println("failed: ${e.message}") // e.codeName is the machine channel, e.status the category
}
// A fallible call answers (value, error); a runtime failure is a
// *clikart.Error. Branch on the code NAME, never the message text.
wrong := must(api.TensorOnes([]int64{5, 7}, clikart.Float32,
clikart.StreamOrDevice{}, clikart.StreamOrDevice{}))
if _, err := clikart.Matmul(a, wrong); err != nil {
var e *clikart.Error
if errors.As(err, &e) {
fmt.Printf("failed: %s\n", e.Message) // e.CodeName is the machine channel, e.Status the category
}
}
// f_op_matmul consumes its tensor args; `a.shallow_clone()` keeps `a` live.
let wrong = api.tensor_full(&[5, 7], 1.0, F32)?;
if let Err(e) = api.f_op_matmul(a.shallow_clone(), wrong, api.absent(), false,
sys::clika_rt_activation_CLIKA_RT_ACTIVATION_IDENTITY, false, false, 0.0, 0.0) {
println!("failed: {e}"); // e.code_name is the machine channel, e.status the category
}
Catch it where you can act on it. The programs in this series let a failure terminate the process, the right default for a small batch program. The message names the operation and the values it refused (here the two shapes); the stable, machine-readable channel is the code name, which Handle errors by code covers.
Next: part 2, tensors and the ops:: library properly.