C++ Services and Events
Defining a service
To define a service:
- Make a struct or class. It may optionally publicly inherit from psibase::Service to gain the psibase::Service::emit and psibase::Service::events convenience methods.
- Define or declare the set of methods
- Reflect the methods using
PSIO_REFLECT
. Only reflected methods become actions; these are available for transactions and for other services to call using psibase::Actor or psibase::call.
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":
- Using actions in a transaction to enter the service
- Using psibase::call or psibase::Actor to enter or reenter the service
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 fromService
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
andreceiver
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.
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.
Example use:
auto result = to<OtherServiceClass>(otherServiceAccount).someMethod(args...);
psibase::from
Actor<EmptyService> psibase::from(
AccountNumber u = AccountNumber()
);
Call a service.
- If
u
is0
(the default), then use this service's authority (getReceiver). - If
u
is non-0, then useu
'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
), orT
: 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
), orT
: 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.