Features¶
This page describes the key features of the Zuspec Software backend and how they work.
Component Translation¶
Basic Components¶
Zuspec Components are translated to C structs with associated functions:
@zdc.dataclass
class Calculator(zdc.Component):
result: int = zdc.field(default=0)
def add(self, a: int, b: int) -> int:
self.result = a + b
return self.result
Generates:
typedef struct Calculator {
int result;
} Calculator;
void Calculator_init(Calculator *self);
int Calculator_add(Calculator *self, int a, int b);
Type Mapping¶
Zuspec to C Type Conversion¶
The backend automatically maps Zuspec types to C types:
Zuspec Type |
C Type |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Example:
@zdc.dataclass
class TypedComponent(zdc.Component):
flag: bool = zdc.field(default=False)
count: int = zdc.field(default=0)
data: zdc.bit32 = zdc.field(default=0)
Generates:
typedef struct TypedComponent {
int flag;
int count;
uint32_t data;
} TypedComponent;
Protocol Interfaces¶
Protocol Types as APIs¶
Protocol types become C API structs with function pointers:
from typing import Protocol
class DataIF(Protocol):
def send(self, data: int) -> int: ...
def receive(self) -> int: ...
@zdc.dataclass
class Sender(zdc.Component):
api: DataIF = zdc.export()
Generates:
// Function pointer table
typedef struct DataIF_vtbl {
int (*send)(void *self, int data);
int (*receive)(void *self);
} DataIF_vtbl;
// Interface wrapper
typedef struct DataIF {
void *impl;
DataIF_vtbl *vtbl;
} DataIF;
// Sender component
typedef struct Sender {
DataIF api;
} Sender;
Async/Sync Processing¶
Async Method Handling¶
Methods decorated with async are analyzed and converted:
@zdc.dataclass
class AsyncComponent(zdc.Component):
async def send_data(self, value: int):
print(f"Sending: {value}")
await self.wait(100)
print("Done")
The backend provides two strategies:
1. Sync Conversion (Optimization)
If the async function doesn’t actually await anything, it’s converted to regular sync:
void AsyncComponent_send_data(AsyncComponent *self, int value) {
printf("Sending: %d\\n", value);
// wait removed since it's the only await
printf("Done\\n");
}
2. State Machine (Full Async)
If the function has real async operations, generates a state machine:
enum AsyncComponent_send_data_states {
STATE_INIT,
STATE_AFTER_WAIT,
STATE_DONE
};
void AsyncComponent_send_data_step(AsyncComponent *self, int value) {
switch (self->_send_data_state) {
case STATE_INIT:
printf("Sending: %d\\n", value);
// Schedule wait
self->_send_data_state = STATE_AFTER_WAIT;
break;
case STATE_AFTER_WAIT:
printf("Done\\n");
self->_send_data_state = STATE_DONE;
break;
}
}
Async Analysis Report¶
The generator prints an analysis report showing conversion decisions:
Async Analysis Report:
======================
Component: AsyncComponent
send_data: Converted to sync (simple wait pattern)
process: Requires state machine (complex async)
Memory Management¶
Component Lifecycle¶
Each component gets initialization and cleanup:
// Initialization
void Component_init(Component *self) {
self->field1 = 0;
self->field2 = default_value;
}
// Optional cleanup
void Component_cleanup(Component *self) {
// Free any allocated resources
}
Stack vs Heap Allocation¶
Components can be allocated on stack or heap:
// Stack allocation
MyComponent comp;
MyComponent_init(&comp);
MyComponent_method(&comp);
// Heap allocation
MyComponent *comp = malloc(sizeof(MyComponent));
MyComponent_init(comp);
MyComponent_method(comp);
free(comp);
Validation¶
Pre-Generation Checks¶
The CValidator checks compatibility before generation:
validator = CValidator()
if not validator.validate(ctxt):
for error in validator.errors:
print(f"Error: {error}")
Validation checks:
All types can be mapped to C
Method signatures are C-compatible
No unsupported Python features
Protocol interfaces are well-formed
Example validation error:
ValidationError: Type 'MyGeneric[T]' cannot be mapped to C
ValidationError: Method 'process' uses unsupported *args parameter
Compilation¶
Built-in Compiler Interface¶
The CCompiler class provides compilation support:
from zuspec.be.sw import CCompiler
from pathlib import Path
compiler = CCompiler(output_dir=Path("build"))
result = compiler.compile(
sources=[Path("comp.c"), Path("main.c")],
output=Path("build/test"),
extra_includes=[Path("include")],
extra_libs=["-lm"] # Link math library
)
if result.success:
print("Compilation successful!")
else:
print(f"Errors:\\n{result.stderr}")
Compiler Options:
sources: List of C source files to compileoutput: Output executable pathextra_includes: Additional include directoriesextra_libs: Extra libraries to linkoptimize: Optimization level (0-3)
The compiler uses GCC by default and includes the zuspec runtime.
Test Execution¶
Automated Testing¶
The TestRunner executes and validates generated code:
from zuspec.be.sw import TestRunner
runner = TestRunner()
result = runner.run(
executable=Path("build/test"),
expected_output="Expected output text",
timeout=10
)
if result.passed:
print("✅ Test passed")
print(f"Output: {result.stdout}")
else:
print("❌ Test failed")
print(f"Expected: {result.expected}")
print(f"Got: {result.stdout}")
Test runner features:
Captures stdout and stderr
Pattern matching for expected output
Timeout support
Return code checking
Detailed result reporting
Type Specialization¶
Monomorphization (Experimental)¶
Enable specialization to generate type-specific code:
generator = CGenerator(
output_dir=Path("output"),
enable_specialization=True
)
This creates specialized versions of generic code, potentially improving performance by eliminating dynamic dispatch.
Without specialization:
void process_data(void *data, int type) {
switch (type) {
case TYPE_INT: /* ... */
case TYPE_FLOAT: /* ... */
}
}
With specialization:
void process_data_int(int *data) {
// Specialized for int
}
void process_data_float(float *data) {
// Specialized for float
}
Note
Type specialization is experimental and may increase code size.
Output Organization¶
File Structure¶
Generated files are organized by component:
output/
├── component1.h # Component header
├── component1.c # Component implementation
├── protocol1.h # Protocol API header
├── main.c # Test harness
└── runtime/ # Zuspec runtime files
Header Files¶
Each component gets a header with:
Type definitions (structs)
Function declarations
Include guards
Required includes
Implementation Files¶
Implementation files contain:
Function definitions
Static helper functions
Initialization code
Method implementations
Runtime Integration¶
Zuspec Runtime¶
Generated code links with the zuspec runtime, which provides:
Async scheduling primitives
Memory management helpers
Print/IO functions
Time simulation support
The runtime is automatically included by the compiler.
Debugging Support¶
Source Comments¶
Generated C includes comments linking back to Python:
/* From: my_component.py:42 MyComponent.process */
void MyComponent_process(MyComponent *self) {
/* ... */
}
Compile with Debug Symbols¶
result = compiler.compile(
sources,
output,
extra_flags=["-g", "-O0"] # Debug symbols, no optimization
)
Use GDB for debugging:
gdb ./build/test
(gdb) break MyComponent_process
(gdb) run
Performance Considerations¶
Optimization Levels¶
Control C compiler optimization:
# Debug build
result = compiler.compile(sources, output, optimize=0)
# Release build
result = compiler.compile(sources, output, optimize=3)
Async Performance¶
Sync-converted async is fastest (no overhead)
State machine async has small overhead
Many small awaits can be costly
Consider batch operations
Memory Usage¶
Stack allocation is faster than heap
Small components fit on stack
Large or long-lived components need heap
No automatic garbage collection