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.
include/etch.h)include/etch.hpp)array[T] | EtchValue* elements | etch_value_new_array() | etch_value_array_*() |
| option[T] | EtchValue | etch_value_new_some() / _none() | etch_value_option_unwrap() |
| result[T, E] | EtchValue | etch_value_new_ok() / _err() | etch_value_result_unwrap_*() |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.
cd examples/capi
make all
This builds multiple examples demonstrating various API features:
simple_example - Basic C API usagehost_functions_example - Registering C functionscpp_example - C++ wrapper demonstrationvm_inspection_example - VM inspection and callbacksdebug_example - Debug server integration#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;
}
#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"
// 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:
verbose: Enable detailed compilation and execution logging (0=off, 1=on)debug: Enable debug mode (0=release with level 2 optimizations, 1=debug with level 1 optimizations)// C++ API
etch::Context ctx; // Automatically cleaned up via RAII
ctx.setVerbose(true);
// 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
// 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);
// 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);
debug=true (or call etch_context_set_debug(ctx, 1)).ETCH_DEBUG_PORT=<port> (and optionally ETCH_DEBUG_TIMEOUT=<ms>) before compiling.etch_execute and repeated etch_call_function invocations,
so you can attach to long-running hosts (see demo/main.cpp) without restarting.// 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
// 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
The C API can now construct and inspect Etch arrays plus the option and result
monads without writing Etch glue code.
// 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.
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.
// 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");
// 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);
Monitor VM execution at the instruction level for profiling, debugging, or tracing.
// Called before each VM instruction
// Return 0 to continue execution, non-zero to stop
typedef int (*EtchInstructionCallback)(EtchContext ctx, void* userData);
// Set instruction callback (called before each VM instruction)
void etch_set_instruction_callback(EtchContext ctx,
EtchInstructionCallback callback,
void* userData);
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);
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);
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);
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.
// 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;
}
| 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() |
etch_context_new() or etch_context_new_with_options(), free with etch_context_free()etch_value_new_*(), free with etch_value_free()etch_free_string()gcc -o myapp myapp.c -I/path/to/etch/include -L/path/to/etch/lib -letch -lm
clang -o myapp myapp.c -I/path/to/etch/include -L/path/to/etch/lib -letch -lm
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).
The current API is not thread-safe. Each thread should have its own EtchContext.
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
{.exportc, cdecl, dynlib.} pragmasSee examples/capi/ for complete working examples:
Build all examples:
cd examples/capi
make all
examples/capi/README.md: Detailed examples and usageinclude/etch.h: C API documentation (inline comments)include/etch.hpp: C++ API documentation (inline comments)✅ Fully backward compatible - All existing code continues to work ✅ No breaking changes - New functions are additions only ✅ Tested - All examples pass with new features
For questions, issues, or contributions: