Skip to content

Methods

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

Methods

Overview

Methods are the primary mechanism for inter-service calls in SERP. They provide a typed, thread-safe way to invoke functionality across service boundaries — locally within a process or transparently across processes via transport.

The current primitive is serp::Call<Context, Ret, Args...>. The legacy primitive serp::Method is still supported but deprecated in new code.


serp::Call

template<typename Context, typename Ret, typename... Args>
class serp::Call;
Template Parameter Description
Context The service type that owns this call (used for routing and diagnostics).
Ret Return type. Use void for fire-and-forget. Use serp::Response<T> for async returns.
Args... Argument types passed to the handler.

serp::Call instances are declared as members of the generated interface class (IService). The generated ServiceBase class wires each call to a handler lambda automatically.


Registering a Handler

Use setHandler() to bind an implementation lambda to a call. This is typically done in onInitialize() or the service constructor via the generated base class:

greet.setHandler([this](std::string msg) {
    logInfo()->info("Got: " + msg);
});

The lambda runs on the owning service's EventLoop. Access to service-owned state inside the lambda is safe without additional locking.


Call Patterns

Fire-and-Forget (void return)

The caller dispatches the call and does not wait for completion. The handler executes asynchronously on the callee's EventLoop.

// Interface declaration (generated)
serp::Call<MyService, void, std::string> greet;

// Handler registration (in MyService)
greet.setHandler([this](std::string msg) {
    logInfo()->info("Hello: " + msg);
});

// Caller (from any service or thread)
myService.greet("world");

Async Call with Callback

For calls that produce a result, the return type is serp::Response<T>. The caller provides a callback that is invoked on the caller's EventLoop when the result is ready.

// Interface declaration (generated)
serp::Call<MyService, serp::Response<int>, std::string> compute;

// Handler registration
compute.setHandler([this](std::string input, serp::Response<int> response) {
    int result = heavyComputation(input);
    response.resolve(result);
});

// Caller with callback
myService.compute("data", [](int result) {
    // Called on caller's EventLoop
    use(result);
});

Subscribe Pattern

Some generated interfaces expose calls that follow a subscribe/notify pattern, where the caller registers interest and receives future updates via the callback mechanism.

myService.subscribeToStatus([this](Status s) {
    updateDisplay(s);
});

Blocking Wait with Timeout

For cases where blocking is acceptable (e.g., initialization sequences, CLI tools), a call can be awaited synchronously with a timeout:

auto result = myService.compute("data").wait(std::chrono::seconds(5));
if (result.hasValue()) {
    use(result.value());
} else {
    // Timed out or rejected
}

Blocking calls should never be issued from within a service's own EventLoop — this will deadlock.


serp::Response<T>

serp::Response<T> is the async result carrier passed to method handlers that need to return a value asynchronously. The handler may capture it and resolve it later — for example after an I/O operation completes.

call.setHandler([this](std::string input, serp::Response<std::string> response) {
    // Dispatch async work; capture response by value
    processAsync(input, [response](std::string result) mutable {
        response.resolve(result);
    });
});
Method Description
response.resolve(value) Deliver a successful result to the caller.
response.reject(error) Signal failure to the caller.

Response is move-only. It may be captured by value in lambdas and passed across thread boundaries safely.


Generated Interface Structure

The SerpIDL code generator produces two classes per service:

IMyService — the pure interface exposed to callers:

class IMyService {
public:
    serp::Call<MyService, void, std::string> greet;
    serp::Call<MyService, serp::Response<int>, std::string> compute;
    serp::Property<double> temperature;
    serp::Notification<std::string, int> trackChanged;
};

MyServiceBase — the base class for the implementation, which wires handlers:

class MyServiceBase : public IMyService, public serp::Service {
protected:
    // Override these in your implementation:
    virtual void onGreet(std::string msg) = 0;
    virtual void onCompute(std::string input, serp::Response<int> response) = 0;
};

You implement MyService : public MyServiceBase and override the on* methods.


Cross-Process Calls

When a service is hosted in a remote process, the caller holds a proxy generated from the same interface. The call syntax is identical — the transport layer handles serialization and delivery transparently:

// Local call and remote call look the same to the caller:
myService.greet("hello");        // local
remoteMyService.greet("hello");  // via transport (IPC/network)

Serialization is generated from the SerpIDL type definitions. No manual marshaling code is required.


Legacy: serp::Method

serp::Method is the pre-Phase-2 untyped call primitive. It accepts arguments as a variant bag and returns results via a callback. New services should use serp::Call instead.


See Also

Clone this wiki locally