Calling Other Services (C++)

Services may synchronously call into other services, for example to transfer tokens, fetch database rows, or even to use common library facilities (this example).

Arithmetic Service

Let's start by breaking up our previous example into a header file (.hpp) and an implementation file (.cpp). This makes it easier for other services to call into it.

arithmetic.hpp

#include <psibase/psibase.hpp>

// The header includes the class definition
struct Arithmetic
{
   // The account this service is normally installed on
   static constexpr auto service = psibase::AccountNumber("arithmetic");

   // The header declares but doesn't implement the actions
   int32_t add(int32_t a, int32_t b);
   int32_t multiply(int32_t a, int32_t b);

   std::optional<psibase::HttpReply> serveSys(psibase::HttpRequest request);
};

// The header does the reflection
PSIO_REFLECT(  //
    Arithmetic,
    method(add, a, b),
    method(multiply, a, b),
    method(serveSys, request))

// Do NOT include PSIBASE_DISPATCH in the header

arithmetic.cpp

#include "arithmetic.hpp"

// The implementation file has the action definitions
int32_t Arithmetic::add(int32_t a, int32_t b)
{
   return a + b;
}

int32_t Arithmetic::multiply(int32_t a, int32_t b)
{
   return a * b;
}

std::optional<psibase::HttpReply> Arithmetic::serveSys(  //
    psibase::HttpRequest request)
{
   return serveSimpleUI<Arithmetic, true>(request);
}

// The implementation file has the dispatcher
PSIBASE_DISPATCH(Arithmetic)

Caller Service

This service calls into the arithmetic service.

caller.hpp

#include <psibase/psibase.hpp>

struct Caller
{
   static constexpr auto service = psibase::AccountNumber("caller");

   int32_t mult_add(int32_t a, int32_t b, int32_t c, int32_t d);

   std::optional<psibase::HttpReply> serveSys(psibase::HttpRequest request);
};

PSIO_REFLECT(  //
    Caller,
    method(mult_add, a, b, c, d),
    method(serveSys, request))

caller.cpp

// This service
#include "caller.hpp"

// Other service
#include "arithmetic.hpp"

int32_t Caller::mult_add(int32_t a, int32_t b, int32_t c, int32_t d)
{
   // This allows us to call into the Arithmetic service. It fetches
   // the account number from Arithmetic::service.
   auto otherService = psibase::to<Arithmetic>();

   // Compute the result. Calls into the Arithmetic service 3 times.
   return otherService.add(          //
       otherService.multiply(a, b),  //
       otherService.multiply(c, d));
}

std::optional<psibase::HttpReply> Caller::serveSys(  //
    psibase::HttpRequest request)
{
   return serveSimpleUI<Caller, true>(request);
}

PSIBASE_DISPATCH(Caller)

CMakeLists.txt

CMakeLists.txt is almost the same as previous examples, but instead of building 1 service, it builds 2.

Building

This is the same as before.

mkdir build
cd build
cmake `psidk-cmake-args` ..
make -j $(nproc)

Deploying the service

Let's deploy both services.

psibase deploy -ip arithmetic arithmetic.wasm
psibase deploy -ip caller caller.wasm

See the psibase deploy docs for more details.

Trying the service

Follow the instructions at trying the service to view the development UI.

What's Happening?

When a service calls another, the system pauses its execution and runs that other service. The system returns the result back to the original caller and resumes execution. This behavior is core to most of psibase's functionality. e.g. the SystemService::Transact service receives a transaction then calls a service for each action within the transaction. These services may call more services, creating a tree of actions.

"Action" may refer to:

  • One of the actions (requests) within a transaction
  • A call from one service to another
  • A method on a service

The system keeps each service alive during the entire transaction. This allows some interesting capabilities:

  • Services may call each other many times without repeating the WASM startup overhead.
  • Services may call into each other recursively. Be careful; you need to either plan for this or disable it. TODO: make it easy for services to opt out of recursion.
  • A service method may store intermediate results in global variables then return. Future calls to that service, within the same transaction, have access to those global variables. They're wiped at the end of the transaction.

Who called me?

A service may call psibase::getSender to find out which service or user called it. A service also may use psibase::getReceiver to get the account that the service is running on.

void MyService::doSomething()
{
   psibase::check(
       psibase::getSender() == expectedAccount,
       "you're not who I expected");
}