C API
Looking for the cross-language FFI contract? See
./ffi-abi.mdforcall_native, the"ERROR:"prefix, the 4 MiBBUFFER_SIZE, and thekcl_ffi.hexport list. The headerkcl_lib.his a thin C wrapper over that single ABI; every typed helper funnels throughkcl_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 cdylibcbindgen(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.
| Helper | RPC | Response |
|---|---|---|
kcl_ping(value, out, out_size) | KclService.Ping | out = PingResult.value |
kcl_get_version(version) | KclService.GetVersion | struct KclVersion |
kcl_exec_program(files, n, yaml, …, err, …) | KclService.ExecProgram | yaml = 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.FormatCode | out = formatted |
kcl_lint_path(paths, n, out, out_size) | KclService.LintPath | out is newline-separated results |
kcl_parse_file(filename, ast, ast_size) | KclService.ParseFile | ast = ast_json |
kcl_parse_program(files, n, ast, ast_size) | KclService.ParseProgram | ast = 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.