Architecture Overview
Understanding Circuit's architecture will help you build better applications and debug issues more effectively.
High-Level Architecture
Circuit consists of four main layers:
┌─────────────────────────────────────────────────────┐
│ Platform Layer (Swift/Kotlin/JS) │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────┐ │
│ │ iOS │ │ Android │ │ Web │ │
│ └──────────────┘ └──────────────┘ └──────────┘ │
└─────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────┐
│ FFI/WASM Bindings Layer │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────┐ │
│ │ circuit-ffi │ │ circuit-ffi │ │circuit- │ │
│ │ (Swift) │ │ (Kotlin) │ │ wasm │ │
│ └──────────────┘ └──────────────┘ └──────────┘ │
└─────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────┐
│ Language Layer │
│ ┌──────────────┐ │
│ │ circuit-lang │ │
│ │ Parser & │ │
│ │ Converter │ │
│ └──────────────┘ │
└─────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────┐
│ Core Runtime Engine │
│ ┌──────────────┐ │
│ │ circuit-core │ │
│ │ Engine, │ │
│ │ Blocks, │ │
│ │ Graphs │ │
│ └──────────────┘ │
└─────────────────────────────────────────────────────┘
Core Components
1. circuit-core: The Runtime Engine
The heart of Circuit, circuit-core provides:
Blocks
The fundamental unit of computation. Each block:
- Has inputs (data it receives)
- Has outputs (data it produces)
- Has configuration (parameters that customize behavior)
- Implements an execute method (the actual computation)
#![allow(unused)] fn main() { pub trait Block: Send + Sync { fn metadata(&self) -> BlockMetadata; fn execute(&self, context: BlockContext) -> Result<HashMap<String, Value>>; fn validate(&self, _config: &HashMap<String, Value>) -> Result<()> { Ok(()) } } }
Graphs
A directed acyclic graph (DAG) representing data flow:
- Nodes: Instances of blocks with unique IDs
- Connections: Data paths between node ports
- Cycle Detection: Ensures no circular dependencies
- Validation: Checks that all connections are valid
Engine
The execution runtime that:
- Registers blocks (makes them available)
- Loads graphs (prepares them for execution)
- Executes graphs in topological order
- Routes data between connected nodes
- Returns final output values
Values
Type-safe data containers supporting:
Null,Bool,Int,Float,StringArray,Object(nested structures)Bytes(binary data)- Conversion and serialization via serde
2. circuit-lang: The Declarative Language
Provides a human-friendly way to define blocks and flows:
Parser
Uses Pest grammar to parse .block and .flow files into AST (Abstract Syntax Tree).
Converter
Transforms AST into executable Graph objects that the engine can run.
Benefits
- Declarative: Describe what you want, not how to build it
- Readable: Clear syntax for non-programmers
- Maintainable: Easy to modify flows without recompiling
3. circuit-ffi: Native Platform Bridge
C-compatible FFI layer for iOS and Android:
- C API: Simple functions for create, load, execute, destroy
- Memory Management: Safe handling of strings and pointers
- Error Handling: Proper error propagation across FFI boundary
- Thread Safety: Uses lazy_static for global engine registry
Swift and Kotlin wrappers provide idiomatic APIs on top of the C layer.
4. circuit-wasm: Web Assembly Bridge
WebAssembly bindings for browser and Node.js:
- wasm-bindgen: Automatic JavaScript bindings
- Zero-Copy: Efficient data transfer between JS and Rust
- TypeScript: Full type definitions included
- Promise-based: Async/await friendly API
Data Flow
Here's how data flows through a Circuit application:
1. Define Flow (.flow file or Rust code)
│
├─> Defines nodes (block instances)
├─> Defines connections (data paths)
└─> Defines configuration (block parameters)
2. Parse/Convert (circuit-lang)
│
└─> Converts to Graph object
3. Load Graph (Engine)
│
├─> Validates graph structure
├─> Checks for cycles
└─> Computes topological order
4. Execute Graph (Engine)
│
├─> Executes nodes in topological order
├─> Routes data through connections
└─> Collects final outputs
5. Return Results
│
└─> HashMap<String, HashMap<String, Value>>
Topological Execution
Circuit executes nodes in topological order, ensuring:
- A node only executes after all its dependencies
- Data flows in the correct direction
- No deadlocks or circular dependencies
Example
For this flow:
flow example {
node a: core.constant { value = 1 }
node b: core.constant { value = 2 }
node c: math.add
node d: math.multiply
connect a.value -> c.a
connect b.value -> c.b
connect c.result -> d.a
}
Execution order:
aandbexecute first (no dependencies)cexecutes next (depends onaandb)dexecutes last (depends onc)
Memory Model
Rust Side
- Blocks:
Arc<dyn Block>- Thread-safe, shared references - Graphs: Owned by Engine, stored in HashMap
- Values: Cloned when passed between nodes (cheap for small values)
FFI Boundary
- Strings: Converted between Rust String and C char*
- Pointers: Engine handles stored in global registry
- Memory Safety: Rust manages all allocations
WASM Boundary
- Values: Serialized as JSON across boundary
- Optimization: Uses wasm-bindgen for efficient marshaling
- Memory: WASM linear memory managed by Rust
Error Handling
Circuit uses Rust's Result type throughout:
#![allow(unused)] fn main() { pub type Result<T> = std::result::Result<T, CircuitError>; pub enum CircuitError { BlockNotFound(String), GraphNotFound(String), InvalidInput(String), InvalidConnection(String), CycleDetected, ExecutionError(String), ParseError(String), } }
Errors propagate through the stack and can be caught at the platform layer.
Performance Characteristics
- Graph Loading: O(N + E) where N = nodes, E = edges
- Topological Sort: O(N + E)
- Execution: O(N × B) where B = average block execution time
- Data Flow: Cloning overhead for Values (typically small)
Optimization Opportunities
- Lazy Evaluation: Only execute nodes needed for requested outputs
- Parallelization: Execute independent nodes concurrently
- Caching: Memoize block results for repeated inputs
- Streaming: Support streaming data for large datasets
These are future roadmap items.
Thread Safety
- Engine: Not thread-safe; use one per thread or protect with Mutex
- Blocks: Must be
Send + Sync(thread-safe) - Graphs: Immutable after loading (safe to share)
- Values: Cloned between nodes (no shared mutation)
Next Steps
- Understanding Blocks - Deep dive into blocks
- The Graph Engine - Engine internals
- Creating Custom Blocks - Build your own blocks