Skip to content

Core Concepts

Oleksandr Geronime edited this page Jun 27, 2026 · 3 revisions

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.


Spec-First Development

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.


Service-Oriented Architecture

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.


Transport Abstraction

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.


Code Generation Guarantees

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.


The Runtime is Engineering-Only

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.


Deployment is a Separate Concern

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.


Summary: How the Pieces Fit Together

┌──────────────────────────────────────────────────────────────────┐
│  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.

Clone this wiki locally