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

zdc.bit

uint8_t

zdc.bit8

uint8_t

zdc.bit16

uint16_t

zdc.bit32

uint32_t

zdc.bit64

uint64_t

int

int

bool

int (0/1)

float

float

double

double

str

const char*

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 compile

  • output: Output executable path

  • extra_includes: Additional include directories

  • extra_libs: Extra libraries to link

  • optimize: 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