etch

Etch C/C++ Library API

Overview

Etch can be compiled as a shared/static library and embedded into C and C++ applications as a scripting engine. This enables using Etch as a safe, statically-typed scripting language with compile-time verification, runtime safety checks, full debugging support, and VM introspection capabilities.

Features

Core Capabilities

API Design

C API (include/etch.h)

C++ Wrapper (include/etch.hpp)

Building

Build the Library

From the project root:

# Build shared library (libetch.so / libetch.dylib)
just build-lib

# Build static library (libetch.a)
just build-lib-static

# Build both
just build-libs

The library will be created in the lib/ directory.

Build the Examples

cd examples/capi
make all

This builds multiple examples demonstrating various API features:

Quick Start

C API Example

#include "etch.h"

int main(void) {
    // Create context
    EtchContext ctx = etch_context_new();

    // Compile Etch code
    const char* code =
        "fn main(): int {\n"
        "    print(\"Hello from Etch!\")\n"
        "    return 0\n"
        "}\n";

    if (etch_compile_string(ctx, code, "hello.etch") != 0) {
        printf("Error: %s\n", etch_get_error(ctx));
        etch_context_free(ctx);
        return 1;
    }

    // Execute
    etch_execute(ctx);

    // Clean up
    etch_context_free(ctx);
    return 0;
}

C++ API Example

#include "etch.hpp"

int main() {
    try {
        // Create context (RAII)
        etch::Context ctx;

        // Compile and execute
        ctx.compileString(
            "fn main(): int {\n"
            "    print(\"Hello from Etch!\")\n"
            "    return 0\n"
            "}\n"
        );
        ctx.execute();

    } catch (const etch::Exception& e) {
        std::cerr << "Error: " << e.what() << std::endl;
        return 1;
    }
    return 0;
}

Disable exceptions if your toolchain forbids them by defining ETCH_CPP_EXCEPTIONS 0 before the header. In this mode fatal errors are reported to stderr and the process aborts instead of throwing.

#define ETCH_CPP_EXCEPTIONS 0
#include "etch.hpp"

API Reference

Context Management

Basic Context Creation

// C API - Create context with default settings
EtchContext etch_context_new(void);

// Create context with custom compiler options
EtchContext etch_context_new_with_options(int verbose, int debug);

// Free context and all associated resources
void etch_context_free(EtchContext ctx);

// Set verbose logging
void etch_context_set_verbose(EtchContext ctx, int verbose);

// Set debug mode (enables debug info in bytecode)
void etch_context_set_debug(EtchContext ctx, int debug);

Compiler Options:

// C++ API
etch::Context ctx;  // Automatically cleaned up via RAII
ctx.setVerbose(true);

Example: Release vs Debug Mode

// Create context for production (release mode, optimizations enabled)
EtchContext ctx = etch_context_new_with_options(0, 0);  // verbose=off, debug=off

// Create context for development (debug mode, debug info enabled)
EtchContext ctx = etch_context_new_with_options(1, 1);  // verbose=on, debug=on

// Change settings between compilations
etch_context_set_debug(ctx, 0);  // Switch to release mode

Compilation

// C API - Compile from string or file
int etch_compile_string(EtchContext ctx, const char* source, const char* filename);
int etch_compile_file(EtchContext ctx, const char* path);
// C++ API
ctx.compileString(source, filename);
ctx.compileFile(path);

Execution

// C API
int etch_execute(EtchContext ctx);
EtchValue etch_call_function(EtchContext ctx, const char* name,
                              EtchValue* args, int numArgs);
// C++ API
ctx.execute();
etch::Value result = ctx.callFunction(name, args);

Remote Debugging from C / C++

Value Creation

// C API
EtchValue etch_value_new_int(int64_t v);
EtchValue etch_value_new_float(double v);
EtchValue etch_value_new_bool(int v);
EtchValue etch_value_new_string(const char* v);
EtchValue etch_value_new_char(char v);
EtchValue etch_value_new_nil(void);
EtchValue etch_value_clone(EtchValue v);
// C++ API
etch::Value intVal(42);
etch::Value floatVal(3.14);
etch::Value boolVal(true);
etch::Value stringVal("hello");
etch::Value charVal('x');
etch::Value nilVal;  // Default constructor creates nil

Value Inspection & Extraction

// C API
int etch_value_is_int(EtchValue v);
int etch_value_to_int(EtchValue v, int64_t* out);
const char* etch_value_to_string(EtchValue v);
void etch_value_free(EtchValue v);
// C++ API
if (val.isInt()) {
    int64_t i = val.toInt();
}
std::string s = val.toString();
// Automatic cleanup via RAII

Arrays, Options, and Results

The C API can now construct and inspect Etch arrays plus the option and result monads without writing Etch glue code.

Arrays from C

// Create Etch values for the elements (remember to free them later)
EtchValue numbers[2];
numbers[0] = etch_value_new_int(21);
numbers[1] = etch_value_new_int(21);

// Build the Etch array (copies each element)
EtchValue array = etch_value_new_array(numbers, 2);

// Read elements back (each accessor returns a new handle)
int len = etch_value_array_length(array);  // -> 2
EtchValue first = etch_value_array_get(array, 0);
int64_t value;
etch_value_to_int(first, &value);  // -> 21
etch_value_free(first);

// Mutate arrays in place
etch_value_array_set(array, 1, etch_value_new_int(99));
etch_value_array_push(array, etch_value_new_int(100));

// Clean up
etch_value_free(numbers[0]);
etch_value_free(numbers[1]);
etch_value_free(array);

Each getter (etch_value_array_get) returns a brand-new handle that the caller must free. Mutating APIs (_set, _push) copy the supplied value so ownership remains with the caller.

Option and Result helpers

EtchValue greeting = etch_value_new_string("hi");
EtchValue opt = etch_value_new_some(greeting);

if (etch_value_option_has_value(opt)) {
    EtchValue inner = etch_value_option_unwrap(opt);
    printf("Message: %s\n", etch_value_to_string(inner));
    etch_value_free(inner);
}

EtchValue ok = etch_value_new_ok(etch_value_new_int(1));
EtchValue err = etch_value_new_err(etch_value_new_string("boom"));

if (etch_value_result_is_ok(ok)) {
    EtchValue payload = etch_value_result_unwrap_ok(ok);
    // ...
    etch_value_free(payload);
}

if (etch_value_result_is_err(err)) {
    EtchValue payload = etch_value_result_unwrap_err(err);
    fprintf(stderr, "error: %s\n", etch_value_to_string(payload));
    etch_value_free(payload);
}

etch_value_free(greeting);
etch_value_free(opt);
etch_value_free(ok);
etch_value_free(err);

etch_value_option_unwrap/etch_value_result_unwrap_* always allocate a new handle so that callers can own the extracted payload safely.

Global Variables

// C API
void etch_set_global(EtchContext ctx, const char* name, EtchValue value);
EtchValue etch_get_global(EtchContext ctx, const char* name);
// C++ API
ctx.setGlobal("counter", etch::Value(42));
etch::Value counter = ctx.getGlobal("counter");

Host Functions

// C API - Register a C function callable from Etch
typedef EtchValue (*EtchHostFunction)(EtchContext ctx, EtchValue* args,
                                      int numArgs, void* userData);

int etch_register_function(EtchContext ctx, const char* name,
                           EtchHostFunction callback, void* userData);

// Example:
EtchValue my_add(EtchContext ctx, EtchValue* args, int numArgs, void* userData) {
    int64_t a, b;
    etch_value_to_int(args[0], &a);
    etch_value_to_int(args[1], &b);
    return etch_value_new_int(a + b);
}

etch_register_function(ctx, "my_add", my_add, NULL);

VM Inspection and Instruction Callbacks

Monitor VM execution at the instruction level for profiling, debugging, or tracing.

Callback Type

// Called before each VM instruction
// Return 0 to continue execution, non-zero to stop
typedef int (*EtchInstructionCallback)(EtchContext ctx, void* userData);

Setting Callbacks

// Set instruction callback (called before each VM instruction)
void etch_set_instruction_callback(EtchContext ctx,
                                   EtchInstructionCallback callback,
                                   void* userData);

Inspection Functions

Call these from within your instruction callback to inspect VM state:

// Get current call stack depth
int etch_get_call_stack_depth(EtchContext ctx);

// Get current program counter (instruction index)
int etch_get_program_counter(EtchContext ctx);

// Get number of registers in current frame
int etch_get_register_count(EtchContext ctx);

// Get value of a specific register
EtchValue etch_get_register(EtchContext ctx, int regIndex);

// Get total instruction count executed
int etch_get_instruction_count(EtchContext ctx);

// Get name of current function
const char* etch_get_current_function(EtchContext ctx);

Example: Execution Tracing

int trace_callback(EtchContext ctx, void* userData) {
    int pc = etch_get_program_counter(ctx);
    int depth = etch_get_call_stack_depth(ctx);
    const char* func = etch_get_current_function(ctx);

    printf("PC=%d, Stack=%d, Function=%s\n", pc, depth, func);

    return 0;  // Continue execution
}

// Set callback and execute
etch_set_instruction_callback(ctx, trace_callback, NULL);
etch_execute(ctx);

Example: Conditional Breakpoint

int breakpoint_callback(EtchContext ctx, void* userData) {
    const char* target_func = (const char*)userData;
    const char* current_func = etch_get_current_function(ctx);

    if (strcmp(current_func, target_func) == 0) {
        printf("Breakpoint hit in function: %s\n", current_func);
        printf("PC: %d\n", etch_get_program_counter(ctx));

        // Inspect registers
        for (int i = 0; i < 10; i++) {
            EtchValue reg = etch_get_register(ctx, i);
            if (reg && etch_value_is_int(reg)) {
                int64_t val;
                etch_value_to_int(reg, &val);
                printf("R%d = %lld\n", i, val);
                etch_value_free(reg);
            }
        }

        return 1;  // Stop execution
    }

    return 0;  // Continue
}

// Break when entering "factorial" function
etch_set_instruction_callback(ctx, breakpoint_callback, "factorial");
etch_execute(ctx);

Use Cases

Note: The instruction callback API is fully implemented but not yet integrated into the VM execution loop. To make it fully functional, the VM’s execution loop needs modification to invoke the callback before each instruction.

Error Handling

// C API
const char* etch_get_error(EtchContext ctx);
void etch_clear_error(EtchContext ctx);
// C++ API - Uses exceptions
try {
    ctx.compileFile("script.etch");
} catch (const etch::Exception& e) {
    std::cerr << e.what() << std::endl;
}

Value Types

Etch Type C Type C++ Type Notes
int int64_t int64_t 64-bit signed integer
float double double Double-precision float
bool int bool Boolean (0/1 in C)
char char char Single character
string const char* std::string UTF-8 string
nil - - Null/nil value
array[T] EtchValue* elements std::vector<etch::Value> Use etch_value_new_array() + etch_value_array_*()
option[T] EtchValue std::optional<etch::Value> Use etch_value_new_some() / _none()
result[T, E] EtchValue etch::Result<T, E> (helper) Use etch_value_new_ok() / _err()

Memory Management

C API

C++ API

Linking

Linux

gcc -o myapp myapp.c -I/path/to/etch/include -L/path/to/etch/lib -letch -lm

macOS

clang -o myapp myapp.c -I/path/to/etch/include -L/path/to/etch/lib -letch -lm

Runtime Library Path

Set LD_LIBRARY_PATH (Linux) or DYLD_LIBRARY_PATH (macOS):

export LD_LIBRARY_PATH=/path/to/etch/lib:$LD_LIBRARY_PATH

Or use rpath flags (see examples/capi/Makefile for examples).

Use Cases

  1. Game Scripting - Safe, fast scripting for game logic with live debugging
  2. Application Plugins - Allow users to extend functionality safely
  3. Configuration DSLs - More powerful than JSON, safer than Lua
  4. Data Processing - Type-safe scripts for data transformation
  5. Testing - Script-driven test scenarios with full debugging
  6. Embedded Systems - Lightweight scripting with safety guarantees
  7. Educational - Teach programming with step-by-step debugging
  8. Profiling - Monitor script execution and performance

Thread Safety

The current API is not thread-safe. Each thread should have its own EtchContext.

Performance

Future Enhancements

Completed ✓

In Progress

Planned

Implementation Details

File Structure

include/
  etch.h          # C API header
  etch.hpp        # C++ wrapper
src/
  etch/
    capi.nim      # C API implementation
  etch_lib.nim    # Library entry point
examples/
  capi/
    simple_example.c
    host_functions_example.c
    cpp_example.cpp
    vm_inspection_example.c
    debug_example.c
    Makefile
    README.md
lib/
  libetch.so      # Built shared library

How It Works

  1. Compilation: Etch source → AST → Typechecked AST → Bytecode (with optional debug info)
  2. Execution: Bytecode → Register VM execution
  3. C API: Nim functions exported with {.exportc, cdecl, dynlib.} pragmas
  4. Memory: Nim’s memory management handles internals, C code manages API objects
  5. Debugging: Debug server implements DAP protocol, controls VM execution

Current Limitations

  1. Instruction callbacks: API complete but not integrated into VM execution loop
  2. Host functions: Can be registered but not yet callable from Etch scripts
  3. Thread safety: Not thread-safe (planned for future)
  4. Arrays/tables: Limited C API support (being expanded)

Examples

See examples/capi/ for complete working examples:

Build all examples:

cd examples/capi
make all

Documentation

Compatibility

Fully backward compatible - All existing code continues to work ✅ No breaking changes - New functions are additions only ✅ Tested - All examples pass with new features

Support

For questions, issues, or contributions: