-
Notifications
You must be signed in to change notification settings - Fork 0
Core Concepts
This page covers the mental models that underpin SERP. Understanding these will make the generated code, the workspace layout, and the deployment model straightforward rather than mysterious.
SERP starts with contracts, not implementations.
You write service interfaces in SerpIDL — a typed interface definition language. The code generator reads those specs and produces a complete C++ skeleton. You write business logic into the stubs it creates.
specs/serp.sidl → serpgen → gen/ → src/ (your code)
(contract) (generator) (skeleton) (implementation)
The spec is the single source of truth for method signatures, argument types, events, and properties. If you want to change an interface, you change the spec and regenerate — you never edit the generated files directly.
Practical rule: gen/ is disposable. src/ is yours.
Each SERP service:
- Runs on its own named event loop (single-threaded)
- Exposes a typed interface to other services
- Receives calls through a generated dispatch table
- Has no shared mutable state with other services
Cross-service calls are typed method invocations on interface pointers. There are no global variables, no shared queues, and no direct thread handoffs between services. The framework enforces this boundary at the generated code level.
┌───────────────────────────┐ ┌───────────────────────────┐
│ Service A (EventLoop A) │ │ Service B (EventLoop B) │
│ │ │ │
│ IBar* bar_; │────►│ BarBase (dispatch) │
│ │ │ │ │
│ bar_->doSomething(x, cb) │ │ ▼ │
│ │ │ BarImpl::doSomething │
│ │ │ (your code) │
└───────────────────────────┘ └───────────────────────────┘
The event loop guarantees that a service's methods execute one at a time. You do not need locks inside a service implementation.
The same service code works with multiple transport backends:
| Backend | Description | Use case |
|---|---|---|
| inprocess | Direct function calls, no IPC | Monolith builds, unit tests |
| DBus | Linux D-Bus IPC | Multiprocess on Linux |
| gRPC | gRPC over network sockets | Portable multiprocess, cross-machine |
The transport is selected in the deployment spec, not in the service code. A service never calls DBus::send(...) or gRpc::call(...). It calls bar_->doSomething(x, cb) — the generated adapter translates that into the right wire protocol.
This means you can develop and test entirely in a monolith (fast, debuggable, no IPC setup) and then deploy the same binary set over DBus or gRPC without touching a single line of service code.
Running serpgen generate-workspace follows these rules without exception:
- Everything under
gen/is always overwritten. - Everything under
src/is never touched.
The generated inheritance chain for each service is:
IFoo (gen/interfaces/IFoo.h)
└── FooBase (gen/base/FooBase.h/.cpp)
└── Foo (gen/services/Foo.h/.cpp — generated once, then left alone*)
└── FooImpl (src/components/foo/FooImpl.h/.cpp — yours)
| Class | Location | Who writes it | Regenerated? |
|---|---|---|---|
IFoo |
gen/interfaces/ |
serpgen |
Yes, every time |
FooBase |
gen/base/ |
serpgen |
Yes, every time |
Foo |
gen/services/ |
serpgen |
Yes, every time |
FooImpl |
src/components/foo/ |
You | Never |
IFoo is a pure virtual interface. FooBase provides the wiring: it connects the transport adapter to the virtual methods, handles serialization, and manages the event loop integration. Foo is a thin default implementation (may be a no-op stub or a generated starting point). FooImpl is where you write business logic — it inherits from Foo and overrides only the methods you care about.
SERP includes a debug registry (Serp::runtime) that lets you call any registered service method by name at runtime, inspect properties, and inject test signals. This is exposed through the VS Code plugin and through serpgen tooling.
The runtime registry is disabled in release builds.
It exists to make development and debugging fast — you can poke a running service without writing a dedicated client. It must never be used in production code paths. If your production code calls Serp::runtime::invoke(...), that is a bug.
A SERP workspace produces multiple build targets from the same service codebase:
- Monolith: all services in one process, inprocess transport
- Per-process binaries: one process per service (or per service group), DBus or gRPC transport
Which services run in which processes, and which transport connects them, is specified in the deployment section of serp.sidl. The service implementation code is identical across all deployment targets.
specs/serp.sidl
├── service Calculator { ... } ← interface and behavior
└── deployment monolith { ← where and how it runs
process main {
includes: [Calculator, Console]
transport: inprocess
}
}
To add a DBus-distributed deployment, you add a second deployment block — no changes to CalculatorImpl.cpp.
┌──────────────────────────────────────────────────────────────────┐
│ SerpIDL spec → serpgen → gen/interfaces/IFoo.h │
│ │
│ IFoo (contract) │
│ implemented by FooBase (generated wiring) │
│ extended by FooImpl (your business logic) │
│ consumed by BarImpl via IFoo* pointer │
│ wired together in gen/mains/main_monolith.cpp │
│ transport connects services across processes │
└──────────────────────────────────────────────────────────────────┘
The spec defines the contract. The generator produces the skeleton. You write the logic. The runtime connects everything. The deployment spec controls the topology.
Getting Started
The Development Model
Architecture Language
Code Generator
- Generator Overview
- Generated Code Layout
- Deployment Configurations
- Lifecycle Backends
- CMake Integration
Framework Internals
- Core Concepts
- Services & Lifecycle
- Methods
- Properties
- Notifications
- Timers & Watchdog
- Promises & Async
- Streams
- Commands
- Logging
- Test Engine
- Transports
- Runtime & Debug Tools
VS Code Plugin
Examples