Skip to main content
Version: Next

C API

Looking for the cross-language FFI contract? See ./ffi-abi.md for call_native, the "ERROR:" prefix, the 4 MiB BUFFER_SIZE, and the kcl_ffi.h export list. The header kcl_lib.h is a thin C wrapper over that single ABI; every typed helper funnels through kcl_call("KclService.<Name>", …).

The C binding ships as a single-header kcl_lib.h (static-inline implementations) plus the nanopb-generated spec.pb.{h,c} and the kcl_ffi.h Rust dispatcher exports. It links against the prebuilt libkcl_lib_c.<so|dylib|lib> shared library built from crates/api via cbindgen.

Prerequisites​

  • make
  • A C11 compiler (gcc / clang / MSVC)
  • cargo (Rust toolchain) for building the cdylib
  • cbindgen (only when re-generating headers)

Building​

git clone --depth 1 https://github.com/kcl-lang/lib.git /tmp/lib
cd /tmp/lib/c
make # builds the Rust cdylib and runs the C examples

The cdylib export (call_native) is generated by cbindgen; see the top of include/kcl_ffi.h for the source of truth.

Buffer size​

#define BUFFER_SIZE (4 * 1024 * 1024)   // 4 MiB

Every typed helper allocates three 4 MiB buffers (buffer, result_buffer, plus per-call scratch). Payloads larger than 4 MiB must use the low-level pattern with a larger result_buffer. See ./ffi-abi.md for the buffer-size contract.

Quick Start​

#include <kcl_lib.h>
#include <stdio.h>

int main(void) {
char yaml_out[BUFFER_SIZE] = {0};
char err_out[BUFFER_SIZE] = {0};
const char* files[] = {"./schema.k"};
if (!kcl_exec_program(files, 1, yaml_out, sizeof(yaml_out),
err_out, sizeof(err_out))) {
fprintf(stderr, "%s\n", err_out);
return 1;
}
printf("%s\n", yaml_out);
return 0;
}

Compile:

cc -I include -L target/release -l kcl_lib_c -o hello hello.c
LD_LIBRARY_PATH=target/release ./hello

API Reference​

The header ships 8 typed wrappers (kcl_*) plus a low-level kcl_call dispatcher and nanopb encode/decode helpers. Every typed wrapper is a static inline that encodes its args via nanopb, calls kcl_call("KclService.<Name>", …), checks the "ERROR:" prefix, and decodes the response.

HelperRPCResponse
kcl_ping(value, out, out_size)KclService.Pingout = PingResult.value
kcl_get_version(version)KclService.GetVersionstruct KclVersion
kcl_exec_program(files, n, yaml, …, err, …)KclService.ExecProgramyaml = yaml_result, err = err_message
kcl_validate_code(code, data, ok, err, …)KclService.ValidateCode*ok = success, err = err_message
kcl_format_code(source, out, out_size)KclService.FormatCodeout = formatted
kcl_lint_path(paths, n, out, out_size)KclService.LintPathout is newline-separated results
kcl_parse_file(filename, ast, ast_size)KclService.ParseFileast = ast_json
kcl_parse_program(files, n, ast, ast_size)KclService.ParseProgramast = ast_json (envelope)

Every helper returns false on failure and copies the error message into the relevant out / err buffer.

kcl_ping​

#include <kcl_lib.h>
#include <stdio.h>

char out[BUFFER_SIZE];
if (kcl_ping("hello", out, sizeof(out))) {
printf("%s\n", out); // -> "hello"
}

kcl_get_version​

#include <kcl_lib.h>

struct KclVersion v;
if (kcl_get_version(&v)) {
printf("version=%s checksum=%s git_sha=%s\n", v.version, v.checksum, v.git_sha);
}

The exact version / checksum / git_sha values vary per release; assert only on non-empty. The KclVersion struct also carries a version_info[1024] field for the human-readable summary.

kcl_exec_program​

#include <kcl_lib.h>

char yaml[BUFFER_SIZE] = {0};
char err[BUFFER_SIZE] = {0};
const char* files[] = {"./schema.k"};
if (!kcl_exec_program(files, 1, yaml, sizeof(yaml), err, sizeof(err))) {
fprintf(stderr, "%s\n", err);
}

Internally: encodes ExecProgramArgs{k_filename_list: […]}, sends to "KclService.ExecProgram", decodes ExecProgramResult and writes yaml_result / err_message into the supplied buffers.

kcl_validate_code​

#include <kcl_lib.h>

bool ok = false;
char err[BUFFER_SIZE] = {0};
if (kcl_validate_code(
"schema Person:\n age: int\n check:\n 0 < age < 120\n",
"{\"age\": 10}", &ok, err, sizeof(err))) {
printf("ok=%d err=%s\n", ok, err);
}

kcl_format_code​

#include <kcl_lib.h>

char out[BUFFER_SIZE];
if (kcl_format_code("schema A:\n x:int\n", out, sizeof(out))) {
printf("%s\n", out);
}

kcl_lint_path​

#include <kcl_lib.h>

char out[BUFFER_SIZE];
const char* paths[] = {"./schema.k"};
if (kcl_lint_path(paths, 1, out, sizeof(out))) {
puts(out); // newline-separated lint results
}

kcl_parse_file / kcl_parse_program​

#include <kcl_lib.h>

char ast[BUFFER_SIZE];
if (kcl_parse_file("./schema.k", ast, sizeof(ast))) {
puts(ast); // AST JSON string
}

char env[BUFFER_SIZE];
const char* files[] = {"./schema.k"};
if (kcl_parse_program(files, 1, env, sizeof(env))) {
puts(env); // AST envelope (paths, root, errors, …)
}

Low-level pattern (kcl_call + nanopb)​

For RPCs without a typed helper (e.g. format_path, override_file, rename, …), drive the dispatcher directly with nanopb. This is the canonical way to add a new RPC and exactly mirrors what every typed helper does internally.

exec_program — end-to-end​

The full pattern from the v0.13.0 header:

#include <kcl_lib.h>

int exec_file(const char* file_str) {
uint8_t buffer[BUFFER_SIZE];
uint8_t result_buffer[BUFFER_SIZE];
size_t message_length;
bool status;
struct Buffer file = {
.buffer = file_str,
.len = strlen(file_str),
};
struct Buffer* files[] = { &file };
struct RepeatedString strs = { .repeated = &files[0], .index = 0, .max_size = 1 };
ExecProgramArgs args = ExecProgramArgs_init_zero;
args.k_filename_list.funcs.encode = encode_str_list;
args.k_filename_list.arg = &strs;

pb_ostream_t stream = pb_ostream_from_buffer(buffer, sizeof(buffer));
status = pb_encode(&stream, ExecProgramArgs_fields, &args);
message_length = stream.bytes_written;

if (!status) {
printf("Encoding failed: %s\n", PB_GET_ERROR(&stream));
return 1;
}

const char* api_str = "KclService.ExecProgram";
size_t result_length = kcl_call(api_str, buffer, message_length, result_buffer);
if (check_error_prefix(result_buffer)) {
printf("%s", result_buffer);
return 1;
}
pb_istream_t istream = pb_istream_from_buffer(result_buffer, result_length);

ExecProgramResult result = ExecProgramResult_init_default;

uint8_t yaml_value_buffer[BUFFER_SIZE] = { 0 };
result.yaml_result.arg = yaml_value_buffer;
result.yaml_result.funcs.decode = decode_string;

uint8_t json_value_buffer[BUFFER_SIZE] = { 0 };
result.json_result.arg = json_value_buffer;
result.json_result.funcs.decode = decode_string;

uint8_t err_value_buffer[BUFFER_SIZE] = { 0 };
result.err_message.arg = err_value_buffer;
result.err_message.funcs.decode = decode_string;

uint8_t log_value_buffer[BUFFER_SIZE] = { 0 };
result.log_message.arg = log_value_buffer;
result.log_message.funcs.decode = decode_string;

status = pb_decode(&istream, ExecProgramResult_fields, &result);

if (!status) {
printf("Decoding failed: %s\n", PB_GET_ERROR(&istream));
return 1;
}

if (result.yaml_result.arg) {
printf("%s\n", (char*)result.yaml_result.arg);
}

return 0;
}

int main(void)
{
return exec_file("./test_data/schema.k");
}

validate_code — end-to-end​

#include <kcl_lib.h>

int validate(const char* code_str, const char* data_str)
{
uint8_t buffer[BUFFER_SIZE];
uint8_t result_buffer[BUFFER_SIZE];
size_t message_length;
bool status;

ValidateCodeArgs validate_args = ValidateCodeArgs_init_zero;
validate_args.code.funcs.encode = encode_string;
validate_args.code.arg = (void*)code_str;
validate_args.data.funcs.encode = encode_string;
validate_args.data.arg = (void*)data_str;

pb_ostream_t stream = pb_ostream_from_buffer(buffer, sizeof(buffer));
status = pb_encode(&stream, ValidateCodeArgs_fields, &validate_args);
message_length = stream.bytes_written;

if (!status) {
printf("Encoding failed: %s\n", PB_GET_ERROR(&stream));
return 1;
}

const char* api_str = "KclService.ValidateCode";
size_t result_length = kcl_call(api_str, buffer, message_length, result_buffer);
if (check_error_prefix(result_buffer)) {
printf("%s\n", result_buffer);
return 1;
}
pb_istream_t istream = pb_istream_from_buffer(result_buffer, result_length);
ValidateCodeResult result = ValidateCodeResult_init_default;

result.err_message.funcs.decode = decode_string;
uint8_t value_buffer[BUFFER_SIZE] = { 0 };
result.err_message.arg = value_buffer;

status = pb_decode(&istream, ValidateCodeResult_fields, &result);

if (!status) {
printf("Decoding failed: %s\n", PB_GET_ERROR(&istream));
return 1;
}

printf("Validate Status: %d\n", result.success);
if (result.err_message.arg) {
printf("Validate Error Message: %s\n", (char*)result.err_message.arg);
}
return 0;
}

int main(void)
{
const char* code_str = "schema Person:\n"
" name: str\n"
" age: int\n"
" check:\n"
" 0 < age < 120\n";
const char* data_str = "{\"name\": \"Alice\", \"age\": 10}";
const char* error_data_str = "{\"name\": \"Alice\", \"age\": 1110}";
validate(code_str, data_str);
validate(code_str, error_data_str);
return 0;
}

Notes​

The legacy BuildProgram and ExecArtifact RPCs were removed from spec/spec.proto in v0.13.0 (see lib commit 815acac); they are no longer recognised by kcl_call. If you previously called kcl_call("KclService.BuildProgram", …), switch to KclService.ExecProgram and decode ExecProgramResult.