Skip to content

Latest commit

 

History

History
1654 lines (1321 loc) · 46.1 KB

README.md

File metadata and controls

1654 lines (1321 loc) · 46.1 KB

C SDK API reference

Complete API reference for the BoxLite C SDK.

Overview

The C SDK provides C-compatible FFI bindings for integrating BoxLite into C/C++ applications.

Library: libboxlite Header: boxlite.h C Standard: C11-compatible compiler (GCC/Clang)

API styles

The SDK provides two API styles:

  1. Simple API (boxlite_simple_*) - Convenience layer for common use cases

    • No runtime setup required
    • Auto-managed runtime
    • Buffered command results
  2. Native API (boxlite_*) - Full-featured, flexible interface

    • Typed CBoxliteOptions configuration
    • Streaming output callbacks
    • Advanced features (volumes, networking, etc.)

Table of contents


Quick start

Simple API (recommended)

#include <stdio.h>
#include "boxlite.h"

int main() {
    CBoxliteSimple* box = NULL;
    CBoxliteError error = {0};

    // Create box and auto-start it
    if (boxlite_simple_new("python:slim", 0, 0, &box, &error) != Ok) {
        fprintf(stderr, "Error %d: %s\n", error.code, error.message);
        boxlite_error_free(&error);
        return 1;
    }

    // Run command and get buffered result
    const char* args[] = {"-c", "print('Hello from BoxLite!')", NULL};
    CBoxliteExecResult* result = NULL;

    if (boxlite_simple_run(box, "python", args, 2, &result, &error) == Ok) {
        printf("Output: %s\n", result->stdout_text);
        printf("Exit code: %d\n", result->exit_code);
        boxlite_result_free(result);
    }

    boxlite_simple_free(box);  // Auto-cleanup
    return 0;
}

Native API (full control)

#include <stdio.h>
#include "boxlite.h"

void output_callback(const char* text, int is_stderr, void* user_data) {
    FILE* stream = is_stderr ? stderr : stdout;
    fprintf(stream, "%s", text);
}

int main() {
    CBoxliteRuntime* runtime = NULL;
    CBoxHandle* box = NULL;
    CBoxliteError error = {0};

    // Create runtime
    if (boxlite_runtime_new(NULL, NULL, 0, &runtime, &error) != Ok) {
        fprintf(stderr, "Error %d: %s\n", error.code, error.message);
        boxlite_error_free(&error);
        return 1;
    }

    // Create box with typed options
    CBoxliteOptions* opts = NULL;
    if (boxlite_options_new("alpine:3.19", &opts, &error) != Ok) {
        fprintf(stderr, "Error %d: %s\n", error.code, error.message);
        boxlite_error_free(&error);
        boxlite_runtime_free(runtime);
        return 1;
    }
    boxlite_options_set_network_enabled(opts);
    CAdvancedBoxOptions* advanced = NULL;
    if (boxlite_advanced_options_new(&advanced, &error) != Ok) {
        boxlite_options_free(opts);
        return 1;
    }
    const char* cap_add[] = {"NET_ADMIN"};
    const char* cap_drop[] = {"NET_RAW"};
    if (boxlite_advanced_options_set_capabilities_add(advanced, cap_add, 1) != Ok ||
        boxlite_advanced_options_set_capabilities_drop(advanced, cap_drop, 1) != Ok) {
        fprintf(stderr, "Invalid Linux capability list\n");
        boxlite_advanced_options_free(advanced);
        boxlite_options_free(opts);
        return 1;
    }
    boxlite_options_set_advanced(opts, advanced);
    boxlite_advanced_options_free(advanced);

    if (boxlite_create_box(runtime, opts, &box, &error) != Ok) {
        fprintf(stderr, "Error %d: %s\n", error.code, error.message);
        boxlite_error_free(&error);
        boxlite_options_free(opts);
        boxlite_runtime_free(runtime);
        return 1;
    }
    boxlite_options_free(opts);

    // Start command with streaming output, then wait for completion
    int exit_code = 0;
    const char* args[] = {"-la", "/"};
    BoxliteCommand cmd = {.command = "/bin/ls", .args = args, .argc = 2};
    CExecutionHandle* execution = NULL;

    if (boxlite_execute(box, &cmd, output_callback, NULL, &execution, &error) == Ok) {
        if (boxlite_execution_wait(execution, &exit_code, &error) == Ok) {
            printf("\nExit code: %d\n", exit_code);
        }
        boxlite_execution_free(execution);
    }
    if (error.code != Ok) {
        fprintf(stderr, "Error: %s\n", error.message);
        boxlite_error_free(&error);
    }

    // Cleanup
    boxlite_runtime_free(runtime);
    return 0;
}

Building

# Compile with the BoxLite library
gcc -I/path/to/boxlite/sdks/c/include \
    -L/path/to/boxlite/target/release \
    -lboxlite \
    my_program.c -o my_program

# macOS: Set library path
export DYLD_LIBRARY_PATH=/path/to/boxlite/target/release:$DYLD_LIBRARY_PATH

# Linux: Set library path
export LD_LIBRARY_PATH=/path/to/boxlite/target/release:$LD_LIBRARY_PATH

Error handling

The C SDK introduces structured error handling with error codes and detailed messages.

BoxliteErrorCode

All API functions return BoxliteErrorCode to indicate success or failure type:

typedef enum BoxliteErrorCode {
    Ok = 0,               // Success
    Internal = 1,         // Internal error
    NotFound = 2,         // Resource not found
    AlreadyExists = 3,    // Resource already exists
    InvalidState = 4,     // Invalid state for operation
    InvalidArgument = 5,  // Invalid argument
    Config = 6,           // Configuration error
    Storage = 7,          // Storage error
    Image = 8,            // Image error
    Network = 9,          // Network error
    Execution = 10,       // Execution error
    Stopped = 11,         // Resource stopped
    Engine = 12,          // Engine error
    Unsupported = 13,     // Unsupported operation
    Database = 14,        // Database error
    Portal = 15,          // Portal/communication error
    Rpc = 16,             // RPC error
    RpcTransport = 17,    // RPC transport error
    Metadata = 18,        // Metadata error
    UnsupportedEngine = 19, // Unsupported engine error
} BoxliteErrorCode;

CBoxliteError

Detailed error information for debugging:

typedef struct CBoxliteError {
    BoxliteErrorCode code;  // Error code for programmatic handling
    char* message;          // Detailed message (NULL if none)
} CBoxliteError;

Error handling patterns

Pattern 1: Basic Check

CBoxliteError error = {0};
BoxliteErrorCode code = boxlite_simple_new("alpine:3.19", 0, 0, &box, &error);

if (code != Ok) {
    fprintf(stderr, "Error %d: %s\n", error.code, error.message);
    boxlite_error_free(&error);
    return 1;
}

Pattern 2: Switch on Error Code

BoxliteErrorCode code = boxlite_get(runtime, "box-id", &box, &error);

switch (code) {
    case Ok:
        // Success - use box
        break;
    case NotFound:
        fprintf(stderr, "Box not found\n");
        break;
    case InvalidState:
        fprintf(stderr, "Box in invalid state\n");
        break;
    default:
        fprintf(stderr, "Error %d: %s\n", error.code, error.message);
}

boxlite_error_free(&error);

Pattern 3: Retry Logic

int retries = 3;
for (int i = 0; i < retries; i++) {
    code = boxlite_simple_new("alpine:3.19", 0, 0, &box, &error);
    if (code == Ok) break;

    fprintf(stderr, "Retry %d/%d: %s\n", i+1, retries, error.message);
    boxlite_error_free(&error);

    if (code == InvalidArgument || code == Unsupported) {
        break;  // Non-retryable errors
    }
    sleep(1);  // Backoff
}

Simple API

The Simple API provides a streamlined interface for common use cases.

boxlite_simple_new

Create and auto-start a box with sensible defaults.

BoxliteErrorCode boxlite_simple_new(
    const char* image,
    int cpus,
    int memory_mib,
    CBoxliteSimple** out_box,
    CBoxliteError* out_error
);

Parameters

Parameter Type Description
image const char* OCI image reference (e.g., "python:slim", "alpine:3.19")
cpus int Number of CPUs (0 = default: 2)
memory_mib int Memory in MiB (0 = default: 512)
out_box CBoxliteSimple** Output: created box handle
out_error CBoxliteError* Output: error information

Returns

BoxliteErrorCode - Ok on success, error code on failure.

Example

CBoxliteSimple* box = NULL;
CBoxliteError error = {0};

// Default resources
if (boxlite_simple_new("alpine:3.19", 0, 0, &box, &error) != Ok) {
    fprintf(stderr, "Error: %s\n", error.message);
    boxlite_error_free(&error);
    return 1;
}

// Custom resources
if (boxlite_simple_new("python:slim", 4, 2048, &box, &error) != Ok) {
    // Handle error
}

boxlite_simple_run

Run a command and get buffered result.

BoxliteErrorCode boxlite_simple_run(
    CBoxliteSimple* box,
    const char* command,
    const char* const* args,
    int argc,
    CBoxliteExecResult** out_result,
    CBoxliteError* out_error
);

Parameters

Parameter Type Description
box CBoxliteSimple* Box handle from boxlite_simple_new
command const char* Command to execute
args const char* const* NULL-terminated array of arguments
argc int Number of arguments (excluding NULL terminator)
out_result CBoxliteExecResult** Output: execution result
out_error CBoxliteError* Output: error information

Result structure

typedef struct CBoxliteExecResult {
    int exit_code;       // Command exit code
    char* stdout_text;   // Standard output
    char* stderr_text;   // Standard error
} CBoxliteExecResult;

Example

const char* args[] = {"-c", "print('hello')", NULL};
CBoxliteExecResult* result = NULL;

if (boxlite_simple_run(box, "python", args, 2, &result, &error) == Ok) {
    printf("stdout: %s\n", result->stdout_text);
    printf("stderr: %s\n", result->stderr_text);
    printf("exit: %d\n", result->exit_code);
    boxlite_result_free(result);
}

boxlite_simple_free

Free a simple box (auto-stops and removes).

void boxlite_simple_free(CBoxliteSimple* box);

Safe to call with NULL.


boxlite_result_free

Free an execution result.

void boxlite_result_free(CBoxliteExecResult* result);

Safe to call with NULL.


Native API

Runtime management

boxlite_version

Get BoxLite version string.

const char* boxlite_version(void);

Returns static string (do not free). Example: "0.5.7".


boxlite_runtime_new

Create a new runtime instance.

typedef enum BoxliteRegistryTransport {
    BoxliteRegistryTransportHttps = 0,
    BoxliteRegistryTransportHttp = 1,
} BoxliteRegistryTransport;

typedef struct BoxliteImageRegistry {
    const char* host;
    BoxliteRegistryTransport transport;
    int skip_verify;
    int search;
    const char* username;
    const char* password;
    const char* bearer_token;
} BoxliteImageRegistry;

BoxliteErrorCode boxlite_runtime_new(
    const char* home_dir,
    const BoxliteImageRegistry* image_registries,
    int image_registries_count,
    CBoxliteRuntime** out_runtime,
    CBoxliteError* out_error
);

Parameters

Parameter Type Description
home_dir const char* Path to BoxLite home. NULL = default (~/.boxlite)
image_registries const BoxliteImageRegistry* Optional registry transport, TLS, search, and auth settings
image_registries_count int Number of entries in image_registries
out_runtime CBoxliteRuntime** Output: runtime handle
out_error CBoxliteError* Output: error information

Example

CBoxliteRuntime* runtime = NULL;
CBoxliteError error = {0};

// Default configuration
if (boxlite_runtime_new(NULL, NULL, 0, &runtime, &error) != Ok) {
    fprintf(stderr, "Error: %s\n", error.message);
    boxlite_error_free(&error);
    return 1;
}

// Custom registries
BoxliteImageRegistry image_registries[] = {
  {
    .host = "ghcr.io",
    .transport = BoxliteRegistryTransportHttps,
    .skip_verify = 0,
    .search = 1,
    .username = NULL,
    .password = NULL,
    .bearer_token = NULL,
  },
  {
    .host = "registry.example.com",
    .transport = BoxliteRegistryTransportHttps,
    .skip_verify = 0,
    .search = 0,
    .username = "user",
    .password = "password",
    .bearer_token = NULL,
  },
};
if (boxlite_runtime_new("/var/lib/boxlite", image_registries, 2, &runtime, &error) != Ok) {
    // Handle error
}

boxlite_runtime_shutdown

Gracefully stop all running boxes.

BoxliteErrorCode boxlite_runtime_shutdown(
    CBoxliteRuntime* runtime,
    int timeout,
    CBoxliteError* out_error
);

Parameters

Parameter Type Description
runtime CBoxliteRuntime* Runtime instance
timeout int Seconds: 0=default(10), -1=infinite, >0=custom
out_error CBoxliteError* Output: error information

boxlite_runtime_free

Free a runtime instance.

void boxlite_runtime_free(CBoxliteRuntime* runtime);

Safe to call with NULL. Automatically frees all boxes.


Box management

boxlite_create_box

Create and auto-start a box.

BoxliteErrorCode boxlite_create_box(
    CBoxliteRuntime* runtime,
    CBoxliteOptions* opts,
    CBoxHandle** out_box,
    CBoxliteError* out_error
);

Parameters

Parameter Type Description
runtime CBoxliteRuntime* Runtime instance
opts CBoxliteOptions* Box options created with boxlite_options_new()
out_box CBoxHandle** Output: box handle
out_error CBoxliteError* Output: error information

Example

CBoxliteOptions* opts = NULL;
if (boxlite_options_new("alpine:3.19", &opts, &error) != Ok) {
    // Handle error
}
boxlite_options_set_cpus(opts, 2);
boxlite_options_set_memory(opts, 512);
boxlite_options_set_network_enabled(opts);
CAdvancedBoxOptions* advanced = NULL;
if (boxlite_advanced_options_new(&advanced, &error) != Ok) {
    boxlite_options_free(opts);
    return 1;
}
const char* cap_add[] = {"NET_ADMIN"};
const char* cap_drop[] = {"NET_RAW"};
if (boxlite_advanced_options_set_capabilities_add(advanced, cap_add, 1) != Ok ||
    boxlite_advanced_options_set_capabilities_drop(advanced, cap_drop, 1) != Ok) {
    fprintf(stderr, "Invalid Linux capability list\n");
    boxlite_advanced_options_free(advanced);
    boxlite_options_free(opts);
    return 1;
}
boxlite_options_set_advanced(opts, advanced);
boxlite_advanced_options_free(advanced);

CBoxHandle* box = NULL;
if (boxlite_create_box(runtime, opts, &box, &error) != Ok) {
    fprintf(stderr, "Error: %s\n", error.message);
    boxlite_error_free(&error);
}
boxlite_options_free(opts);

boxlite_start_box

Start or restart a stopped box.

BoxliteErrorCode boxlite_start_box(
    CBoxHandle* handle,
    CBoxliteError* out_error
);

boxlite_stop_box

Stop a running box.

BoxliteErrorCode boxlite_stop_box(
    CBoxHandle* handle,
    CBoxliteError* out_error
);

Note: Consumes the handle - do not use after calling.


boxlite_remove

Remove a box.

BoxliteErrorCode boxlite_remove(
    CBoxliteRuntime* runtime,
    const char* id_or_name,
    int force,
    CBoxliteError* out_error
);
Parameter Type Description
id_or_name const char* Box ID (full or prefix) or name
force int Non-zero to force remove running box

boxlite_get

Reattach to an existing box.

BoxliteErrorCode boxlite_get(
    CBoxliteRuntime* runtime,
    const char* id_or_name,
    CBoxHandle** out_handle,
    CBoxliteError* out_error
);

boxlite_box_id

Get box ID string from handle.

char* boxlite_box_id(CBoxHandle* handle);

Important: Caller must free with boxlite_free_string().


boxlite_box_free

Free a box handle.

void boxlite_box_free(CBoxHandle* handle);

Safe to call with NULL. Use when you need to release a box handle without freeing the entire runtime.


Network tunnels

Creation takes a complete system sockaddr_storage and a BoxliteTunnelOptions containing bool is_public. Initialize the address with boxlite_socket_address_parse("127.0.0.1:3000", &target, &error) (IPv6 uses "[::1]:3000"). NULL options mean public. The parser performs no DNS resolution and leaves output unchanged on error. Inputs are borrowed for the call; never cast a smaller sockaddr_in/sockaddr_in6 allocation to the storage parameter. Targets require a nonzero port, loopback or guest IP, and zero IPv6 scope/flow information. See the C example.

BoxliteErrorCode boxlite_box_network(
    CBoxHandle* handle,
    CBoxNetworkHandle** out_network,
    CBoxliteError* out_error
);
void boxlite_network_free(CBoxNetworkHandle* network);

BoxliteErrorCode boxlite_socket_address_parse(
    const char* address,
    struct sockaddr_storage* out_address,
    CBoxliteError* out_error
);

BoxliteErrorCode boxlite_network_tunnel(
    CBoxNetworkHandle* network,
    const struct sockaddr_storage* target,
    const BoxliteTunnelOptions* options,
    CBoxTunnelHandle** out_tunnel,
    CBoxliteError* out_error
);
BoxliteErrorCode boxlite_network_revoke_tunnel(
    CBoxNetworkHandle* network,
    uint16_t port,
    CBoxliteError* out_error
);
BoxliteErrorCode boxlite_tunnel_uri(
    CBoxTunnelHandle* tunnel,
    char** out_uri,
    CBoxliteError* out_error
);
BoxliteErrorCode boxlite_tunnel_connect(
    CBoxTunnelHandle* tunnel,
    int32_t* out_fd,
    CBoxliteError* out_error
);
void boxlite_tunnel_free(CBoxTunnelHandle* tunnel);

BoxliteErrorCode boxlite_tunnel_forward(
    CBoxTunnelHandle* tunnel,
    const BoxliteSocketAddress* listen,
    CTunnelForwarderHandle** out_forwarder,
    CBoxliteError* out_error
);
BoxliteErrorCode boxlite_tunnel_forwarder_address(
    CTunnelForwarderHandle* forwarder,
    char** out_address,
    CBoxliteError* out_error
);
BoxliteErrorCode boxlite_tunnel_forwarder_wait(
    CTunnelForwarderHandle* forwarder,
    CTunnelForwarderWaitCb cb,
    void* user_data,
    CBoxliteError* out_error
);
BoxliteErrorCode boxlite_tunnel_forwarder_close(
    CTunnelForwarderHandle* forwarder,
    CTunnelForwarderCloseCb cb,
    void* user_data,
    CBoxliteError* out_error
);
void boxlite_tunnel_forwarder_free(CTunnelForwarderHandle* forwarder);

Each CBoxTunnelHandle is one-shot. Choose boxlite_tunnel_connect() or boxlite_tunnel_forward(); either consumes the prepared tunnel. The target storage and typed options are described above. Read uri, isPublic, and token with boxlite_tunnel_descriptor(), before or after connect/forward consumes the tunnel, freeing the result with boxlite_free_string(). Only creation returns the full token; later preparations return a mask. Forwarding opens connections against the existing declaration and cannot undo revocation. boxlite_tunnel_uri() reports where a remotely served tunnel can be reached, as an allocated string the caller frees with boxlite_free_string(). It writes NULL for a local tunnel.

boxlite_network_revoke_tunnel() withdraws the public or private tunnel of port. It is safe to repeat, leaves open connections running, and does nothing for a local box.

boxlite_tunnel_forward() creates a TCP or Unix listener from the tunnel. Its wait and close callbacks are posted exactly once through the parent runtime's drain queue; freeing the caller handle requests non-blocking cancellation while already accepted operations retain their callback state. This differs from boxlite_options_add_port(), which creates a persistent, local-only host listener that accepts repeated connections.


Command execution

boxlite_execute

Start a command with optional streaming output and return an execution handle.

typedef struct BoxliteCommand {
    const char* command;      // Required: command to execute
    const char* const* args;  // Argument array, or NULL
    int argc;                 // Number of entries in args
    const char* const* env_pairs; // [key0, value0, key1, value1, ...], or NULL
    int env_count;            // Number of strings in env_pairs
    const char* workdir;      // Working directory, or NULL
    const char* user;         // User spec (e.g., "nobody", "1000:1000"), or NULL
    double timeout_secs;      // Timeout in seconds (0.0 = no timeout)
    int tty;                  // 0 = no TTY, non-zero = TTY
} BoxliteCommand;

BoxliteErrorCode boxlite_execute(
    CBoxHandle* handle,
    const BoxliteCommand* cmd,
    void (*callback)(const char* text, int is_stderr, void* user_data),
    void* user_data,
    CExecutionHandle** out_execution,
    CBoxliteError* out_error
);

Parameters

Parameter Type Description
handle CBoxHandle* Box handle
cmd const BoxliteCommand* Command descriptor
callback function pointer Optional streaming output callback
user_data void* User data passed to callback
out_execution CExecutionHandle** Output: execution handle
out_error CBoxliteError* Output: error information

Callback signature

void callback(const char* text, int is_stderr, void* user_data);
Parameter Description
text Output text chunk
is_stderr 0 for stdout, 1 for stderr
user_data User data from boxlite_execute

Example

void output_handler(const char* text, int is_stderr, void* data) {
    FILE* stream = is_stderr ? stderr : stdout;
    fprintf(stream, "%s", text);
}

int exit_code = 0;
const char* args[] = {"-c", "print('hello')"};
BoxliteCommand cmd = {.command = "python", .args = args, .argc = 2};
CExecutionHandle* execution = NULL;
BoxliteErrorCode code = boxlite_execute(
    box,
    &cmd,
    output_handler,
    NULL,
    &execution,
    &error
);

if (code == Ok) {
    code = boxlite_execution_wait(execution, &exit_code, &error);
    boxlite_execution_free(execution);
}
if (code == Ok) {
    printf("Exit code: %d\n", exit_code);
}

Execution control

BoxliteErrorCode boxlite_execution_write(CExecutionHandle* execution, const char* data, int len, CBoxliteError* out_error);
BoxliteErrorCode boxlite_execution_wait(CExecutionHandle* execution, int* out_exit_code, CBoxliteError* out_error);
BoxliteErrorCode boxlite_execution_kill(CExecutionHandle* execution, CBoxliteError* out_error);
BoxliteErrorCode boxlite_execution_resize_tty(CExecutionHandle* execution, int rows, int cols, CBoxliteError* out_error);
void boxlite_execution_free(CExecutionHandle* execution);

Example: command options

const char* args[] = {"-c", "import os; print(os.getcwd())"};
const char* env[] = {"MY_VAR", "hello"};
BoxliteCommand cmd = {
    .command = "python",
    .args = args,
    .argc = 2,
    .env_pairs = env,
    .env_count = 2,
    .workdir = "/tmp",
    .user = "nobody",
    .timeout_secs = 30.0,
};

int exit_code = 0;
CExecutionHandle* execution = NULL;
BoxliteErrorCode code =
    boxlite_execute(box, &cmd, output_handler, NULL, &execution, &error);
if (code == Ok) {
    code = boxlite_execution_wait(execution, &exit_code, &error);
    boxlite_execution_free(execution);
}

Discovery & introspection

boxlite_list_info

List all boxes.

BoxliteErrorCode boxlite_list_info(
    CBoxliteRuntime* runtime,
    CBoxInfoListCb callback,
    void* user_data,
    CBoxliteError* out_error
);

Ok means the request was queued. The callback runs later when the caller invokes boxlite_runtime_drain(). On success, the callback owns the non-NULL list and must free it with boxlite_free_box_info_list().


boxlite_get_info

Get single box info by ID or name.

BoxliteErrorCode boxlite_get_info(
    CBoxliteRuntime* runtime,
    const char* id_or_name,
    CBoxInfoCb callback,
    void* user_data,
    CBoxliteError* out_error
);

boxlite_box_info

Get box info from handle.

BoxliteErrorCode boxlite_box_info(
    CBoxHandle* handle,
    CBoxInfoCb callback,
    void* user_data,
    CBoxliteError* out_error
);

All three info functions use the same post-and-drain contract. out_error reports only synchronous queueing failures. The error passed to a callback is borrowed and valid only during that callback; do not call boxlite_error_free() on it. user_data must remain valid until the callback runs.

typedef struct {
    int done;
} InfoRequest;

static void on_box_info(CBoxInfo* info, CBoxliteError* error, void* user_data) {
    InfoRequest* request = user_data;
    if (error->code != Ok) {
        fprintf(stderr, "info failed: %s\n",
                error->message ? error->message : "unknown error");
    } else {
        // The callback owns info on success.
        printf("Box %s status: %s\n", info->id, info->status);
        boxlite_free_box_info(info);
    }
    request->done = 1;
}

InfoRequest request = {0};
CBoxliteError error = {0};
if (boxlite_box_info(box, on_box_info, &request, &error) == Ok) {
    while (!request.done) {
        if (boxlite_runtime_drain(runtime, -1, &error) < 0) {
            fprintf(stderr, "drain failed: %s\n",
                    error.message ? error.message : "unknown error");
            boxlite_error_free(&error);
            break;
        }
    }
} else {
    fprintf(stderr, "queueing info failed: %s\n",
            error.message ? error.message : "unknown error");
    boxlite_error_free(&error);
}

A list callback has the equivalent ownership rule:

static void on_box_list(CBoxInfoList* list, CBoxliteError* error,
                        void* user_data) {
    InfoRequest* request = user_data;
    if (error->code == Ok) {
        for (int i = 0; i < list->count; ++i) {
            printf("%s\n", list->items[i].id);
        }
        boxlite_free_box_info_list(list);
    }
    request->done = 1;
}

InfoRequest list_request = {0};
if (boxlite_list_info(runtime, on_box_list, &list_request, &error) == Ok) {
    while (!list_request.done) {
        if (boxlite_runtime_drain(runtime, -1, &error) < 0) {
            boxlite_error_free(&error);
            break;
        }
    }
} else {
    boxlite_error_free(&error);
}

info->network is an owned CNetworkInfo* freed with the rest of CBoxInfo. It is NULL when network information is unavailable. Otherwise it contains a COutboundNetworkInfo/CInboundNetworkInfo per direction (outbound, inbound), each with a typed mode plus an allow_net string array and count, and a nullable CPublishedPortList*. The deprecated top-level mode, allow_net and allow_net_count fields keep their pre-split offsets and alias outbound. A NULL published_ports pointer means the current handle does not know the lifecycle's publications, a non-NULL list with count == 0 means there are no active local publications, and a populated list contains typed CPublishedPort entries with guest_port, host_ip, host_port, and protocol fields. These nested values are borrowed from CBoxInfo and must not be freed separately.

Because info resolution is asynchronous, the implementation may perform backend I/O before posting the callback. Keep the runtime and any callback user_data alive until the completion has been drained.


Images

boxlite_runtime_images() returns a CBoxliteImageHandle* for the images a runtime can boot from: the local cache on a runtime from boxlite_runtime_new(), the server's catalog on one from boxlite_rest_runtime_new_with_options(). List, get and remove run unchanged against either; pull is local only and usage REST only. Free the handle with boxlite_image_free().

Function Local runtime REST runtime
boxlite_image_pull() Pulls into the cache Unsupported: creating a box pulls
boxlite_image_list() One row per cached reference Catalog alias and digest references
boxlite_image_get() The cache entries under the name The catalog entry with its versions
boxlite_image_remove() Forgets the name; layers stay Removes the catalog entry; InvalidState while a box can boot from it
boxlite_image_usage() Unsupported count, limit, known_bytes
boxlite_image_create_alias() Unsupported Creates a new alias; existing names conflict
boxlite_image_delete_alias() Unsupported Removes only the named alias
BoxliteErrorCode boxlite_image_get(
    CBoxliteImageHandle* handle,
    const char* name,
    CBoxImageGetCb cb,          // void (*)(CImageDetail*, CBoxliteError*, void*)
    void* user_data,
    CBoxliteError* out_error
);

BoxliteErrorCode boxlite_image_remove(
    CBoxliteImageHandle* handle,
    const char* name,
    CBoxImageRemoveCb cb,       // void (*)(CBoxliteError*, void*)
    void* user_data,
    CBoxliteError* out_error
);

BoxliteErrorCode boxlite_image_usage(
    CBoxliteImageHandle* handle,
    CBoxImageUsageCb cb,        // void (*)(CImageUsage*, CBoxliteError*, void*)
    void* user_data,
    CBoxliteError* out_error
);

BoxliteErrorCode boxlite_image_create_alias(
    CBoxliteImageHandle* handle,
    const char* name,
    const char* alias,
    const char* digest,         // "sha256:" and 64 lowercase hex digits
    CBoxImageGetCb cb,          // the image as it now is, as for a get
    void* user_data,
    CBoxliteError* out_error
);

These operations follow the post-and-drain contract of the info functions: Ok means queued, and the callback runs in boxlite_runtime_drain(). name is an image name such as "docker.io/library/alpine", copied before the call returns. A name the runtime does not hold reaches the callback as NotFound. A reference with a tag or digest ("quay.io/acme/app:v1") reaches it as InvalidArgument, since a remove takes every alias of the name. Locally, a box built from a removed image reads the image's configuration from the registry when it next starts. boxlite_image_create_alias() copies alias and digest as it does name; an alias that does not use OCI tag syntax or a malformed digest reaches the callback as InvalidArgument before anything is sent.

Alias creation returns AlreadyExists through the callback for every existing name, even when its digest matches. Explicit deletion uses the image-remove callback shape and removes no versions:

BoxliteErrorCode boxlite_image_delete_alias(
    CBoxliteImageHandle* handle, const char* name, const char* alias,
    CBoxImageRemoveCb cb, void* user_data, CBoxliteError* out_error
);

A catalog fills every field below; a local cache fills a version's aliases and leaves its optional times unset and history empty.

Type Fields Ownership
CImageDetail name, aliases/aliases_count, curated (non-zero for operator-provided images), versions/versions_count (newest first), history/history_count (alias registration history, newest first) The callback owns it; free with boxlite_free_image_detail()
CImageVersion digest, size_bytes (valid when has_size is non-zero), source_ref, recorded_at (Unix seconds), aliases/aliases_count, last_used_at and upstream_missing_at (Unix seconds, valid when has_last_used_at / has_upstream_missing_at is non-zero) Owned by its CImageDetail
CImageAliasEvent alias, from_digest (NULL for alias creation), to_digest, actor (NULL when a pull seeded the alias), recorded_at (Unix seconds) Owned by its CImageDetail
CImageUsage count, limit, known_bytes Borrowed for the callback; nothing to free
static void on_detail(CImageDetail* detail, CBoxliteError* error,
                      void* user_data) {
    int* done = user_data;
    if (error->code == Ok) {
        for (int i = 0; i < detail->versions_count; ++i) {
            printf("%s %s\n", detail->name, detail->versions[i].digest);
        }
        boxlite_free_image_detail(detail);
    } else {
        fprintf(stderr, "get failed: %s\n", error->message);
    }
    *done = 1;
}

CBoxliteImageHandle* images = NULL;
if (boxlite_runtime_images(runtime, &images, &error) == Ok) {
    int done = 0;
    if (boxlite_image_get(images, "docker.io/library/alpine", on_detail,
                          &done, &error) == Ok) {
        while (!done && boxlite_runtime_drain(runtime, -1, &error) >= 0) {
        }
    }
    boxlite_image_free(images);
}
if (error.code != Ok) {
    boxlite_error_free(&error);
}

Registries

boxlite_image_registries() returns a CBoxliteRegistryHandle* for the logins a REST server presents when it pulls a private image for a box. A runtime from boxlite_runtime_new() answers Unsupported: it pulls with the logins in its image_registries option instead. Free the handle with boxlite_registry_free().

BoxliteErrorCode boxlite_registry_list(
    CBoxliteRegistryHandle* handle,
    CBoxRegistryListCb cb,      // void (*)(CRegistryCredentialList*, CBoxliteError*, void*)
    void* user_data,
    CBoxliteError* out_error
);

BoxliteErrorCode boxlite_registry_create(
    CBoxliteRegistryHandle* handle,
    const char* registry_host,      // e.g. "ghcr.io"
    const char* repository_prefix,  // e.g. "acme/"; NULL for the whole registry
    const char* username,
    const char* password,
    CBoxRegistryCreateCb cb,    // void (*)(CRegistryCredential*, CBoxliteError*, void*)
    void* user_data,
    CBoxliteError* out_error
);

BoxliteErrorCode boxlite_registry_remove(
    CBoxliteRegistryHandle* handle,
    const char* id,
    CBoxRegistryRemoveCb cb,    // void (*)(CBoxliteError*, void*)
    void* user_data,
    CBoxliteError* out_error
);

All three follow the post-and-drain contract of the image functions. The strings are copied before the call returns, and the password is sent once and never returned. The callback receives AlreadyExists for a second login for the same registry and prefix, InvalidState (naming the boxes) for removing a login a box still pulls through, and NotFound for an unknown id. An id that is not a UUID reaches it as InvalidArgument without a request.

Type Fields Ownership
CRegistryCredential id, registry_host, repository_prefix (empty for the whole registry), username, created_by (NULL when unknown), created_at (Unix seconds); no password From a create callback, owned; free with boxlite_free_registry_credential()
CRegistryCredentialList items/count The callback owns it; free with boxlite_free_registry_credential_list()

Registry strings are non-null, except created_by when unknown. If any server-returned field contains an interior NUL byte, create and list callbacks receive Internal and no result. The SDK rejects the response without changing credential IDs; list conversion frees any entries already converted.


Guest SSH control

These operations take a CBoxHandle* and follow the existing Native API callback convention.

Function Additional input Callback
boxlite_box_ssh_configure const char* config_json CBoxSshConfigureCb
boxlite_box_ssh_status None CBoxSshStatusCb
boxlite_box_ssh_disable None CBoxSshDisableCb

All three callbacks receive CSshStatus*. Its corresponding release function is boxlite_free_ssh_status(); the existing memory management rules apply.

CSshStatus field C type
enabled int (non-zero when enabled)
generation uint64_t
listen_address char*
host_public_key char*
host_key_fingerprint char*

config_json must be UTF-8 JSON and only needs to remain valid for the call. Malformed JSON or non-UTF-8 input returns InvalidArgument with invalid SSH configuration, without echoing rejected values. Operation failures are reported through the callback.

The configuration fields are listen_address, host_private_key, and accounts of login, authorized_keys, and optional ca with public_key and principal.


Metrics

boxlite_runtime_metrics

Get runtime-wide metrics.

BoxliteErrorCode boxlite_runtime_metrics(
    CBoxliteRuntime* runtime,
    CRuntimeMetrics* out_metrics,
    CBoxliteError* out_error
);

boxlite_box_metrics

Get per-box metrics.

BoxliteErrorCode boxlite_box_metrics(
    CBoxHandle* handle,
    CBoxMetrics* out_metrics,
    CBoxliteError* out_error
);

Memory management

Rules

  1. All allocated strings must be freed

    • boxlite_box_id() → boxlite_free_string()
  2. Error structs must be freed

    • Caller-owned CBoxliteError output → boxlite_error_free()
    • Callback error pointers are borrowed and must not be freed
  3. Results must be freed

    • CBoxliteExecResult → boxlite_result_free()
    • CBoxInfo → boxlite_free_box_info()
    • CBoxInfoList → boxlite_free_box_info_list()
    • CImagePullResult → boxlite_free_image_pull_result()
    • CImageInfoList → boxlite_free_image_info_list()
    • CImageDetail → boxlite_free_image_detail()
  4. All cleanup functions are NULL-safe

Functions

boxlite_free_string

Free a string allocated by BoxLite.

void boxlite_free_string(char* str);

boxlite_error_free

Free error struct (message only - struct itself is stack-allocated).

void boxlite_error_free(CBoxliteError* error);

boxlite_box_free

Free a box handle.

void boxlite_box_free(CBoxHandle* handle);

Safe to call with NULL.


Thread safety

Component Thread Safety
CBoxliteRuntime Thread-safe
CBoxHandle NOT thread-safe - do not share across threads
CBoxliteSimple NOT thread-safe - do not share across threads
Callbacks Invoked on the thread calling boxlite_runtime_drain()

Safe multi-threaded usage

// CORRECT: Share runtime, create per-thread boxes
void* thread_func(void* arg) {
    CBoxliteRuntime* runtime = (CBoxliteRuntime*)arg;
    CBoxliteError error = {0};
    CBoxliteOptions* opts = NULL;
    CBoxHandle* box = NULL;

    // Each thread creates its own box
    boxlite_options_new("alpine:3.19", &opts, &error);
    boxlite_create_box(runtime, opts, &box, &error);
    boxlite_options_free(opts);
    // Use box in this thread only
    boxlite_stop_box(box, &error);
    return NULL;
}

CBoxliteRuntime* runtime;
boxlite_runtime_new(NULL, NULL, 0, &runtime, &error);

pthread_t threads[4];
for (int i = 0; i < 4; i++) {
    pthread_create(&threads[i], NULL, thread_func, runtime);
}

Platform requirements

Platform Architecture Status Requirements
macOS ARM64 (Apple Silicon) Supported macOS 11.0+, Hypervisor.framework
macOS x86_64 (Intel) Not supported N/A
Linux x86_64 Supported KVM enabled
Linux ARM64 (aarch64) Supported KVM enabled
Windows Any Via WSL2 WSL2 with KVM

Migration from v0.1.x

Error handling change

v0.1.x (old):

char* error = NULL;
CBoxliteRuntime* runtime = boxlite_runtime_new(NULL, &error);
if (!runtime) {
    fprintf(stderr, "Error: %s\n", error);
    boxlite_free_string(error);
    return 1;
}

v0.2.0 (new):

CBoxliteRuntime* runtime = NULL;
CBoxliteError error = {0};
BoxliteErrorCode code = boxlite_runtime_new(NULL, NULL, 0, &runtime, &error);
if (code != Ok) {
    fprintf(stderr, "Error %d: %s\n", error.code, error.message);
    boxlite_error_free(&error);
    return 1;
}

Execute change

v0.1.x:

int exit_code = old_execute_api_returning_exit_code(...);
if (exit_code < 0) {
    // Error
}

v0.2.0:

int exit_code = 0;
const char* args[] = {"hello"};
BoxliteCommand cmd = {.command = "echo", .args = args, .argc = 1};
CExecutionHandle* execution = NULL;
BoxliteErrorCode code =
    boxlite_execute(box, &cmd, callback, NULL, &execution, &error);
if (code == Ok) {
    code = boxlite_execution_wait(execution, &exit_code, &error);
    boxlite_execution_free(execution);
}
if (code != Ok) {
    // Error
}

Migration checklist

  • Replace char* error = NULL with CBoxliteError error = {0}
  • Initialize output pointers to NULL (e.g., CBoxliteRuntime* runtime = NULL)
  • Use output parameters for synchronous calls and callbacks for asynchronous calls
  • Replace return value checks with BoxliteErrorCode checks
  • Replace boxlite_free_string(error) with boxlite_error_free(&error)
  • Create boxes with CBoxliteOptions

API summary

Function Description
boxlite_version() Get version string
boxlite_runtime_new() Create runtime
boxlite_runtime_shutdown() Graceful shutdown
boxlite_runtime_free() Free runtime
boxlite_runtime_metrics() Get runtime metrics
boxlite_create_box() Create box
boxlite_start_box() Start/restart box
boxlite_stop_box() Stop box
boxlite_remove() Remove box
boxlite_get() Reattach to box
boxlite_box_id() Get box ID
boxlite_box_free() Free box handle
boxlite_box_info() Queue box info lookup
boxlite_box_metrics() Get box metrics
boxlite_execute() Execute command
boxlite_list_info() Queue box list lookup
boxlite_get_info() Queue box info lookup by ID
boxlite_runtime_images() Get the image handle
boxlite_image_pull() Queue an image pull (local runtimes)
boxlite_image_list() Queue an image list
boxlite_image_get() Queue a read of an image name's builds
boxlite_image_remove() Queue removal of an image name
boxlite_image_usage() Queue a read of the image allowance (REST runtimes)
boxlite_image_create_alias() Create a catalog alias; existing names conflict (REST runtimes)
boxlite_image_delete_alias() Delete one catalog alias (REST runtimes)
boxlite_image_free() Free the image handle
boxlite_free_image_detail() Free an image detail
boxlite_image_registries() Get the registry login handle (REST runtimes)
boxlite_registry_list() Queue a list of registry logins
boxlite_registry_create() Queue adding a registry login
boxlite_registry_remove() Queue removing a registry login
boxlite_registry_free() Free the registry login handle
boxlite_free_registry_credential() Free a registry login
boxlite_free_registry_credential_list() Free a registry login list
boxlite_simple_new() Create simple box
boxlite_simple_run() Run command (simple)
boxlite_simple_free() Free simple box
boxlite_result_free() Free exec result
boxlite_free_string() Free string
boxlite_error_free() Free error

Common patterns

Streaming output

void output_callback(const char* text, int is_stderr, void* user_data) {
    FILE* stream = is_stderr ? stderr : stdout;
    fprintf(stream, "%s", text);
}

int exit_code = 0;
const char* args[] = {"-c", "print('hello')"};
BoxliteCommand cmd = {.command = "python", .args = args, .argc = 2};
CExecutionHandle* execution = NULL;
if (boxlite_execute(box, &cmd, output_callback, NULL, &execution, &error) == Ok) {
    boxlite_execution_wait(execution, &exit_code, &error);
    boxlite_execution_free(execution);
}

Reattach to box

// Get box ID
char* box_id = boxlite_box_id(box);

// Later, in different process:
CBoxHandle* box2 = NULL;
boxlite_get(runtime, box_id, &box2, &error);

boxlite_free_string(box_id);

Get box info

InfoRequest request = {0};
BoxliteErrorCode code =
    boxlite_box_info(box, on_box_info, &request, &error);
if (code == Ok) {
    while (!request.done) {
        if (boxlite_runtime_drain(runtime, -1, &error) < 0) {
            boxlite_error_free(&error);
            break;
        }
    }
}

Common mistakes

Uninitialized error struct

CBoxliteError error;       // Wrong: uninitialized
CBoxliteError error = {0}; // Correct: zero-initialized

Forgetting to free error

if (code != Ok) {
    printf("Error: %s\n", error.message);
    return 1;                       // Wrong: memory leak
}

if (code != Ok) {
    printf("Error: %s\n", error.message);
    boxlite_error_free(&error);     // Correct
    return 1;
}

Forgetting to free result structs

static void leaking_list_callback(CBoxInfoList* list, CBoxliteError* error,
                                  void* user_data) {
    // Wrong: successful list ownership was transferred here but never freed.
}

static void list_callback(CBoxInfoList* list, CBoxliteError* error,
                          void* user_data) {
    if (error->code == Ok) {
        boxlite_free_box_info_list(list);  // Correct
    }
}

Build & link

CMake

cmake_minimum_required(VERSION 3.15)
project(my_app)

set(BOXLITE_INCLUDE "/path/to/boxlite/sdks/c/include")
set(BOXLITE_LIB_DIR "/path/to/boxlite/target/release")

include_directories(${BOXLITE_INCLUDE})

add_executable(my_app main.c)
target_link_libraries(my_app ${BOXLITE_LIB_DIR}/libboxlite.dylib)

Direct compilation

# macOS
gcc -o myapp myapp.c \
    -I/path/to/boxlite/sdks/c/include \
    -L/path/to/boxlite/target/release \
    -lboxlite

export DYLD_LIBRARY_PATH=/path/to/boxlite/target/release:$DYLD_LIBRARY_PATH
./myapp

# Linux
gcc -o myapp myapp.c \
    -I/path/to/boxlite/sdks/c/include \
    -L/path/to/boxlite/target/release \
    -lboxlite

export LD_LIBRARY_PATH=/path/to/boxlite/target/release:$LD_LIBRARY_PATH
./myapp

See also