C++ Services and Events

Defining a service

To define a service:

Example without convenience base class:

struct MyService
{
   // The account this service is normally installed on. This definition
   // is optional.
   static constexpr auto service = psibase::AccountNumber("myservice");

   void        doSomething(std::string_view str);
   std::string somethingElse(uint32_t x, psibase::AccountNumber y);
};

PSIO_REFLECT(MyService,
             method(doSomething, str),
             method(somethingElse, x, y))

Example with convenience base class:

struct MyService: psibase::Service<MyService>
{
   // The account this service is normally installed on. This definition
   // is optional.
   static constexpr auto service = psibase::AccountNumber("myservice");

   void        doSomething(std::string_view str);
   std::string somethingElse(uint32_t x, psibase::AccountNumber y);
};

PSIO_REFLECT(MyService,
             method(doSomething, str),
             method(somethingElse, x, y))

Calling other services

psibase::Actor supports calling other services. psibase::to and psibase::from simplify obtaining an actor.

To call another service:

auto result =
    psibase::to<OtherServiceClass>(otherServiceAccount)
    .someMethod(args...);

If OtherServiceClass defines service within it, you may omit the account name:

auto result =
    psibase::to<OtherServiceClass>()
    .someMethod(args...);

Defining events

See the following for a description of the various types of events:

To define events for a service, declare the event functions as below, then reflect them using the 4 macros below. Each of the History, Ui, and Merkle structs must be present and reflected, even when they don't have any events declared within.

After you have defined your events, use psibase::Service::emit to emit them and psibase::Service::events to read them.

struct MyService: psibase::Service<MyService> {
   struct Events {
      // Events which live a long time
      struct History {
         // These functions don't need implementations;
         // they only define the interface
         void myEvent(uint32_t a, std::string s);
         void anotherEvent(psibase::AccountNumber account);
      };

      // Events which live a short time
      struct Ui {
         void updateDisplay();
      };

      // Events which live in Merkle trees
      struct Merkle {
         void credit(
            psibase::AccountNumber from,
            psibase::AccountNumber to,
            uint64_t amount);
      };
   };
};

PSIBASE_REFLECT_EVENTS(MyService)

PSIBASE_REFLECT_HISTORY_EVENTS(
   MyService,
   method(myEvent, a, s),
   method(anotherEvent, account))

PSIBASE_REFLECT_UI_EVENTS(
   MyService,
   method(updateDisplay))

PSIBASE_REFLECT_MERKLE_EVENTS(
   MyService,
   method(credit, from, to, amount))

Recursion safety

  • By default, services support recursion. TODO: make it opt-in instead.
  • When a service is called multiple times within a transaction, including recursively, each action gets a fresh DerivedService instance. However, it runs in the same WASM memory space as the other executing actions for that service. Global variables and static variables are shared.
  • Potential hazards to watch out for:
    • If a call modifies member variables within a Service instance, other calls aren't likely to see it.
    • If a call modifies global or static variables, this will effect both the other currently-executing calls, and subsequent calls.
    • If a call modifies the database, other currently-executing calls will see the change only if they read or re-read the database.
    • When you call into any service; assume it can call you back unless you opted out of recursion. TODO: make it possible to opt out of recursion.
    • Calling other services while you are iterating through the database can be dangerous, since they can call back into you, causing you to change the database.

The notes above use the following definition of "call":

Calling service methods directly (e.g. this->doSomething()) don't count in this definition.

psibase::getSender

AccountNumber psibase::getSender();

The account which authorized the currently-executing action.

psibase::getReceiver

AccountNumber psibase::getReceiver();

The account which received the currently-executing action.

psibase::Service

template<typename DerivedService>
struct psibase::Service {
    emit(...);   // Emit events
    events(...); // Read events
};

Services may optionally inherit from this to gain the emit and events convenience methods.

Template arguments:

  • DerivedService: the most-derived service class that inherits from Service

psibase::Service::emit

EventEmitter<DerivedService> psibase::Service::emit() const;

Emit events.

The following examples use the example definitions in Defining Events. After you have defined your events, you can use emit to emit them. Examples:

auto eventANumber = this->emit().history().myEvent(a, s);
auto eventBNumber = this->emit().ui().updateDisplay();
auto eventCNumber = this->emit().merkle().credit(from, to, amount);

These functions return a psibase::EventNumber, aka uint64_t, which uniquely identifies the event. This number supports lookup; see Service::events.

emit is just a convenience method with the following definition:

EventEmitter<DerivedService> emit() const
{
   return EventEmitter<DerivedService>();
}

Here's how to do the above when the service doesn't inherit from psibase::Service:

EventEmitter<MyService> emitter;
auto eventANumber = emitter.history().myEvent(a, s);
auto eventBNumber = emitter.ui().updateDisplay();
auto eventCNumber = emitter.merkle().credit(from, to, amount);

psibase::Service::events

EventReader<DerivedService> psibase::Service::events() const;

Read events.

The following examples use the example definitions in Defining Events. After you have defined your events, you can use events to read them. Examples:

auto eventAArguments = this->events().history().myEvent(eventANumber).unpack();
auto eventBArguments = this->events().ui().updateDisplay(eventBNumber).unpack();
auto eventCArguments = this->events().merkle().credit(eventCNumber).unpack();

These functions take a psibase::EventNumber, aka uint64_t, which uniquely identifies the event. These numbers were generated by Service::emit.

The functions return psio::shared_view_ptr<std::tuple<event argument types>>. You can get the tuple using unpack(), like above.

There are restrictions on when events can be read; see the following for details:

events is just a convenience method with the following definition:

EventReader<DerivedService> events() const
{
   return EventReader<DerivedService>();
}

Here's how to do the above when the service doesn't inherit from psibase::Service:

auto EventReader<MyService> reader;
auto eventAArguments = reader.history().myEvent(eventANumber).unpack();
auto eventBArguments = reader.ui().updateDisplay(eventBNumber).unpack();
auto eventCArguments = reader.merkle().credit(eventCNumber).unpack();

psibase::Actor

template<typename T = void>
struct psibase::Actor {
    AccountNumber sender;   // Use this authority
    AccountNumber receiver; // Send actions to this account

    Actor(...);      // Constructor
    from(...);       // Use `other` authority
    to(...);         // Select a service to send actions to
    operator->(...); // Return this
    operator*(...);  // Return *this
};

Calls other services.

Template arguments:

  • T: the service class for the receiver

Actor methods

Actor uses reflection to get the set of methods on T. It adds methods to itself with the same names, arguments, and return types to simplify calling.

For example, if SomeService has this set of methods:

struct SomeService : psibase::Service<SomeService>
{
   void        doSomething(std::string_view str);
   std::string doAnother(uint32_t x, psibase::AccountNumber y);
};
PSIO_REFLECT(SomeService,
             method(doSomething, str),
             method(doAnother, x, y))

Then Actor<SomeService> will have the same methods. Actor's methods:

  • Pack their arguments, along with sender and receiver into Action
  • Use call to synchronously call receiver with the action data
  • Unpack the return value from the synchonous call
  • Return it

psibase::Actor::Actor

psibase::Actor::Actor(
    AccountNumber sender,
    AccountNumber receiver
);

Constructor.

This actor will send actions to receiver using sender authority.

You probably don't need this constructor; use psibase::to or psibase::from.

Non-priviledged services may only use their own authority.

psibase::Actor::from

Actor<T> psibase::Actor::from(
    AccountNumber other
) const;

Use other authority.

This returns a new Actor object instead of modifying this.

Non-priviledged services may only use their own authority.

psibase::Actor::to

template<typename Other>
Actor<Other> psibase::Actor::to(
    uint64_t otherReceiver
) const;

Select a service to send actions to.

Template arguments:

  • Other: the service's class

Arguments

  • otherReceiver: the account the service runs on

This returns a new Actor object instead of modifying this.

psibase::Actor::operator->

Actor<T> * psibase::Actor::operator->() const;

Return this.

psibase::Actor::operator*

Actor<T> & psibase::Actor::operator*() const;

Return *this.

psibase::to

template<DefinesService Service>
Actor<Service> psibase::to();

Call a service.

Template arguments:

  • Service: the receiver's class

Returns an Actor for calling receiver using the current service's authority. This version sets receiver to Service::service; this works if Service defined a const member named service which identifies the account that service is normally deployed on.

See the other overload

Example use:

auto result = to<OtherServiceClass>().someMethod(args...);

psibase::to

template<typename Service>
Actor<Service> psibase::to(
    AccountNumber receiver
);

Call a service.

Template arguments:

  • Service: the receiver's class

Returns an Actor for calling receiver using the current service's authority.

See the other overload

Example use:

auto result = to<OtherServiceClass>(otherServiceAccount).someMethod(args...);

psibase::from

Actor<EmptyService> psibase::from(
    AccountNumber u = AccountNumber()
);

Call a service.

  • If u is 0 (the default), then use this service's authority (getReceiver).
  • If u is non-0, then use u's authority. Non-priviledged services may only use their own authority.

See psibase::to; it covers the majority use case.

Example use:

auto result =
  from(userAccount)
  .to<OtherServiceClass>(otherServiceAccount)
  .someMethod(args...);

psibase::EventEmitter

template<typename T = void>
struct psibase::EventEmitter {
    EventEmitter(...); // Constructor
    ui(...);           // Emit Ui events
    history(...);      // Emit History events
    merkle(...);       // Emit Merkle events
    from(...);         // Emit events from sender
    operator->(...);   // Return this
    operator*(...);    // Return *this
};

Emits events.

Template arguments:

  • T: the service class which defines the events (e.g. MyService), or
  • T: the inner-most struct within the service class which defines the events (e.g. MyService::Events::History)

Emit methods

EventEmitter uses reflection to get the set of events on T. It adds methods to itself with the same names and arguments.

For example, assume SomeService has the set of events in Defining Events. EventEmitter<MyService> e will support the following:

  • e.history().myEvent(a, s);
  • e.ui().updateDisplay();
  • e.merkle().credit(from, to, amount);

These functions return a psibase::EventNumber, aka uint64_t, which uniquely identifies the event. This number supports lookup; see Service::events.

psibase::EventEmitter::EventEmitter

psibase::EventEmitter::EventEmitter(
    DbId elog = psibase::DbId::historyEvent
);

Constructor.

Initialize the emitter with:

  • elog: the event log to emit to

Service::emit is a shortcut to constructing this.

psibase::EventEmitter::ui

auto psibase::EventEmitter::ui() const;

Emit Ui events.

See Emit Methods

psibase::EventEmitter::history

auto psibase::EventEmitter::history() const;

Emit History events.

See Emit Methods

psibase::EventEmitter::merkle

auto psibase::EventEmitter::merkle() const;

Emit Merkle events.

See Emit Methods

psibase::EventEmitter::from

auto psibase::EventEmitter::from(
    AccountNumber sender
) const;

Emit events from sender.

This returns a new EventEmitter object instead of modifying this.

You probably don't need this; use Service::emit instead.

psibase::EventEmitter::operator->

auto * psibase::EventEmitter::operator->() const;

Return this.

psibase::EventEmitter::operator*

auto & psibase::EventEmitter::operator*() const;

Return *this.

psibase::EventReader

template<typename T = void>
struct psibase::EventReader {
    EventReader(...); // Constructor
    EventReader(...); 
    ui(...);          // Read Ui events
    history(...);     // Read History events
    merkle(...);      // Read Merkle events
    from(...);        // Read events from sender
    operator->(...);  // Return this
    operator*(...);   // Return *this
};

Reads events.

Template arguments:

  • T: the service class which defines the events (e.g. MyService), or
  • T: the inner-most struct within the service class which defines the events (e.g. MyService::Events::History)

Reader methods

EventReader uses reflection to get the set of events on T. It adds methods to itself with the same names and arguments.

For example, assume SomeService has the set of events in Defining Events. EventReader<MyService> e will support the following:

  • auto eventAArguments = e.history().myEvent(eventANumber).unpack();
  • auto eventBArguments = e.ui().updateDisplay(eventBNumber).unpack();
  • auto eventCArguments = e.merkle().credit(eventCNumber).unpack();

These functions take a psibase::EventNumber, aka uint64_t, which uniquely identifies the event. These numbers were generated by Service::emit.

The functions return psio::shared_view_ptr<std::tuple<event argument types>>. You can get the tuple using unpack(), like above.

There are restrictions on when events can be read; see the following for details:

psibase::EventReader::EventReader

psibase::EventReader::EventReader(
    DbId elog = psibase::DbId::historyEvent
);

Constructor.

Initialize the reader with:

  • elog: the event log to read from

Service::events is a shortcut for constructing this.

psibase::EventReader::EventReader

psibase::EventReader::EventReader(
    AccountNumber sender,
    DbId          elog = psibase::DbId::historyEvent
);

psibase::EventReader::ui

auto psibase::EventReader::ui() const;

Read Ui events.

See Reader Methods

psibase::EventReader::history

auto psibase::EventReader::history() const;

Read History events.

See Reader Methods

psibase::EventReader::merkle

auto psibase::EventReader::merkle() const;

Read Merkle events.

See Reader Methods

psibase::EventReader::from

auto psibase::EventReader::from(
    AccountNumber sender
);

Read events from sender.

This returns a new EventReader object instead of modifying this.

You probably don't need this; use Service::events instead.

psibase::EventReader::operator->

auto * psibase::EventReader::operator->() const;

Return this.

psibase::EventReader::operator*

auto & psibase::EventReader::operator*() const;

Return *this.