Embedding Aether Actors in C Applications
This guide explains how to embed Aether actors in your existing C applications. There are two approaches: compiling Aether code to C and linking it, or using the runtime API directly.
Overview
Aether compiles to C, which means integration with C codebases is straightforward:
- Compile-and-Link: Write actors in Aether, compile to C with
aetherc, include the generated code - Direct Runtime API: Use Aether's runtime API to create actors entirely in C
Approach 1: Compile Aether to C
Step 1: Write Your Aether Actor
counter.ae:
message Increment {
amount: int
}
message GetValue {}
message Reset {}
actor Counter {
state count = 0
receive {
Increment(amount) -> {
count = count + amount
}
GetValue() -> {
print(count)
}
Reset() -> {
count = 0
}
}
}
main() {
counter = spawn(Counter())
counter ! Increment { amount: 10 }
counter ! GetValue {}
}
Step 2: Compile to C
aetherc counter.ae counter.c
This generates counter.c containing:
- Message struct definitions
- Actor struct with state
- Message handler dispatch table
spawn_Counter()function
Step 3: Include in Your C Project
main.c:
#include <stdio.h>
#include "runtime/aether_runtime.h"
#include "runtime/scheduler/multicore_scheduler.h"
// Forward declarations from generated code
typedef struct Counter Counter;
Counter* spawn_Counter(void);
// Message IDs (from generated code, numbered from 0 in declaration order)
#define MSG_Increment 0
#define MSG_GetValue 1
#define MSG_Reset 2
int main(int argc, char** argv) {
// Initialize Aether runtime
aether_runtime_init(0, AETHER_FLAG_AUTO_DETECT);
scheduler_init(4); // 4 cores
scheduler_start();
// Spawn the actor
Counter* counter = spawn_Counter();
// Send messages using the Message struct
Message msg = {0};
msg.type = MSG_Increment;
msg.payload_int = 10; // amount
scheduler_send_remote((ActorBase*)counter, msg, -1);
// Allow time for processing
// In real code, use proper synchronization
usleep(100000);
// Cleanup
scheduler_shutdown(); // Waits for quiescence, stops and joins threads
scheduler_cleanup();
aether_runtime_shutdown();
return 0;
}
Step 4: Build
# Compile Aether code
aetherc counter.ae counter.c
# Build with runtime, `ae cflags` emits the right -I / -L / -l for
# the install in effect. Don't hand-craft the linker flags: the
# install layout (`<prefix>/lib/aether/libaether.a`) requires an
# explicit `-L<prefix>/lib/aether` that bare `-laether` won't find.
gcc -O2 main.c counter.c $(ae cflags) -o myapp
Approach 2: Direct Runtime API
For full control, create actors directly in C using the runtime API.
Minimal Example
#include "runtime/aether_runtime.h"
#include "runtime/scheduler/multicore_scheduler.h"
// Define your actor structure
typedef struct {
ActorBase base; // Must be first field
int counter; // Your state
} MyCounter;
// Message handler
void my_counter_step(void* self) {
MyCounter* actor = (MyCounter*)self;
Message msg;
// Process messages from mailbox
while (mailbox_receive(&actor->base.mailbox, &msg)) {
switch (msg.type) {
case 1: // Increment
actor->counter += msg.payload_int;
break;
case 2: // Print
printf("Counter: %d\n", actor->counter);
break;
}
}
}
int main() {
// Initialize runtime
aether_runtime_init(0, AETHER_FLAG_AUTO_DETECT);
scheduler_init(4);
scheduler_start();
// Spawn actor using pool
MyCounter* actor = (MyCounter*)scheduler_spawn_actor(
0, // Preferred core
my_counter_step, // Step function
sizeof(MyCounter) // Size
);
actor->counter = 0; // Initialize state
// Send messages
Message msg = {0};
msg.type = 1; // Increment
msg.payload_int = 42;
scheduler_send_remote((ActorBase*)actor, msg, -1);
msg.type = 2; // Print
scheduler_send_remote((ActorBase*)actor, msg, -1);
// Wait for messages to be processed
scheduler_wait(); // Non-destructive: can send more messages after this
// Cleanup
scheduler_shutdown(); // Waits for quiescence, stops and joins threads
scheduler_cleanup();
aether_runtime_shutdown();
return 0;
}
Runtime API Reference
Lifecycle Functions
// Initialize runtime with automatic CPU feature detection
// num_cores: 0 = auto-detect, or specify explicitly
// flags: AETHER_FLAG_AUTO_DETECT recommended
void aether_runtime_init(int num_cores, int flags);
// Shutdown runtime and free resources
void aether_runtime_shutdown(void);
// Initialize scheduler with core count
// cores: 0 = auto-detect
void scheduler_init(int cores);
// Start scheduler threads
void scheduler_start(void);
// Wait for quiescence (all pending messages processed)
// Non-destructive: scheduler threads keep running. Safe to call multiple times.
void scheduler_wait(void);
// Wait for quiescence, then stop and join scheduler threads.
// Call once at program exit.
void scheduler_shutdown(void);
// Free scheduler resources
void scheduler_cleanup(void);
Actor Management
// Spawn actor from pool (recommended)
// Returns actor pointer, NULL on failure
ActorBase* scheduler_spawn_actor(
int preferred_core, // Core hint, -1 for caller's core (main thread defaults to core 0)
void (*step)(void*), // Message handler function
size_t actor_size // sizeof(YourActorType)
);
// Return actor to pool when done
void scheduler_release_actor(ActorBase* actor);
// Register existing actor (alternative to pooled)
int scheduler_register_actor(ActorBase* actor, int preferred_core);
Message Passing
// Message structure
typedef struct {
int type; // Message ID
int sender_id; // Sender actor ID
intptr_t payload_int; // For single-int messages (intptr_t to pass actor refs without truncation)
void* payload_ptr; // For complex payloads
} Message;
// Send to actor on same core (fast path)
void scheduler_send_local(ActorBase* actor, Message msg);
// Send to actor on any core
// from_core: sender's core ID, -1 if unknown
void scheduler_send_remote(ActorBase* actor, Message msg, int from_core);
Configuration Flags
#define AETHER_FLAG_AUTO_DETECT (1 << 0) // Detect CPU features
#define AETHER_FLAG_LOCKFREE_MAILBOX (1 << 1) // Lock-free mailboxes
#define AETHER_FLAG_LOCKFREE_POOLS (1 << 2) // Lock-free pools
#define AETHER_FLAG_ENABLE_SIMD (1 << 3) // SIMD optimizations
#define AETHER_FLAG_ENABLE_MWAIT (1 << 4) // MWAIT for idle
#define AETHER_FLAG_VERBOSE (1 << 5) // Print config on init
Actor Structure Requirements
When defining actors in C, follow these rules:
typedef struct {
ActorBase base; // MUST be first field
// Your state fields here
int my_value;
char* my_string;
} MyActor;
The ActorBase contains:
active: Processing flag (atomic_int)id: Unique actor IDmailbox: Message queue (ring buffer)step: Your message handlerassigned_core: Current core assignment (atomic_int, safe for cross-core reads)migrate_to: Affinity hint for message-driven migration (atomic_int)main_thread_only: If set, scheduler threads skip this actor (atomic_int)spsc_queue: Lock-free same-core messaging (pointer, lazily allocated)reply_slot: Non-NULL during ask/reply (_Atomicpointer)step_lock: Prevents concurrentstep()during work-steal handoff (atomic_flag)
Message Handler Pattern
void my_actor_step(void* self) {
MyActor* actor = (MyActor*)self;
Message msg;
// Drain all pending messages
while (mailbox_receive(&actor->base.mailbox, &msg)) {
switch (msg.type) {
case MSG_TYPE_1:
// Handle message type 1
handle_type_1(actor, msg.payload_int);
break;
case MSG_TYPE_2:
// Handle message type 2
handle_type_2(actor, (SomeStruct*)msg.payload_ptr);
break;
default:
// Unknown message type
break;
}
}
}
Build Configuration
Makefile Example
ae cflags is the canonical way to obtain the include and link flags it covers in-tree dev builds, ~/.aether user installs, and /usr/local system installs uniformly. Do not hand-craft -I / -L / -l: the install layout puts the archive at <prefix>/lib/aether/ (not flat under <prefix>/lib/), so -laether alone wouldn't find it without an explicit -L.
CC = gcc
AE_CFLAGS := $(shell ae cflags)
# For Aether source files
%.c: %.ae
aetherc $< $@
myapp: main.c counter.c
$(CC) -O2 -o $@ $^ $(AE_CFLAGS)
CMake Example
cmake_minimum_required(VERSION 3.10)
project(MyAetherApp)
set(AETHER_HOME "$ENV{HOME}/.aether")
include_directories(${AETHER_HOME}/include/aether)
link_directories(${AETHER_HOME}/lib)
add_executable(myapp main.c counter.c)
target_link_libraries(myapp aether pthread)
Thread Safety
- Each actor runs on one core at a time
- Messages are delivered via thread-safe queues
- Actor state is only accessed by one thread
- Cross-core sends use lock-free queues
Host-process boundary
The C host and the Aether runtime share a process but have distinct ownership rules. Respect these at every integration point:
- State is message-only. Actor state is owned by the actor; the host reads and writes it exclusively through messages. Reach-in access bypasses message-ordering guarantees and races with the scheduler.
- Sends are fire-and-forget. There's no synchronous reply from
aether_send_message(). When you need a value back, either (a) have the actor send a response message to a host-registered event handler, or (b) have the actor write into shared atomic state the host polls. - References are manually lifetime-managed. The host holds the raw
ActorBase*(or the generatedCounter*) returned by spawn; there's no handle type or automatic refcount across the FFI boundary. Keep the pointer as long as you intend to send to the actor. Release it withscheduler_release_actor()when done. - One runtime per process.
aether_runtime_init()initializes process-global scheduler state. Calling it twice from the same process is not supported; use separate processes if you need isolated runtimes.
Header Generation
Pass --emit-header to aetherc to generate a C header alongside the C output. The header contains message struct definitions, MSG_* constants, and actor spawn prototypes, everything needed to send messages to Aether actors from C without copying struct definitions by hand.
aetherc --emit-header counter.ae counter.c
# Produces: counter.c counter.h
--emit-header must precede the input/output paths. Placed after the positional arguments it is ignored and only counter.c is written.
Example generated header:
// counter.h (auto-generated by aetherc --emit-header)
#ifndef COUNTER_H
#define COUNTER_H
#include <stdint.h>
#include "runtime/scheduler/multicore_scheduler.h"
// Message IDs (numbered from 0 in declaration order)
#define MSG_Increment 0
#define MSG_GetValue 1
#define MSG_Reset 2
// Message structs, first field is the message ID; single-int payloads
// widen to intptr_t to match Message.payload_int.
typedef struct { int _message_id; intptr_t amount; } Increment;
// Spawn prototype
Counter* spawn_Counter(void);
#endif
Include the header in your C host application and use the constants with scheduler_send_remote.
Polling from a C Event Loop
When a C event loop (raylib, SDL, game loop, etc.) holds the main thread, Aether actors in main-thread mode cannot process messages, the scheduler has no opportunity to run.
Use aether_scheduler_poll() to drain pending messages from C code between frames:
#include "runtime/scheduler/multicore_scheduler.h"
// In your render/game loop:
while (!window_should_close()) {
// Drain actor messages, process up to 64 per actor per call.
// Pass 0 for unlimited (processes all pending).
aether_scheduler_poll(64);
begin_drawing();
// ... render ...
end_drawing();
}
// Signature
// max_per_actor: max messages to process per actor per call (0 = unlimited)
// returns: total messages processed across all actors
int aether_scheduler_poll(int max_per_actor);
aether_scheduler_poll is safe to call from the main thread at any time. It only processes actors that are in main-thread mode; scheduler worker threads are not affected. It is a no-op if no actors are registered.
See Also
- C Interoperability - Calling C from Aether
- Runtime Configuration - Runtime flags and options
- Actor Concurrency - Actor model concepts