[ESI][PyCDE] Callback service (#7153)

Call software functions from hardware.
This commit is contained in:
John Demme 2024-06-18 22:14:35 -07:00 committed by GitHub
parent 0e13467021
commit 1f6c29fb64
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 510 additions and 12 deletions

View File

@ -19,6 +19,7 @@ else()
CIRCTPythonModules
ESIRuntime
ESIPythonRuntime
esitester
)
# If ESI Cosim is available to build then enable its tests.

View File

@ -0,0 +1,68 @@
# ===- esitester.py - accelerator for testing ESI functionality -----------===//
#
# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
# See https://llvm.org/LICENSE.txt for license information.
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
#
# ===----------------------------------------------------------------------===//
#
# This accelerator is intended to eventually grow into a full ESI accelerator
# test image. It will be used both to test system functionality and system
# performance. The corresponding software appliciation in the ESI runtime and
# the ESI cosim. Where this should live longer-term is a unclear.
#
# ===----------------------------------------------------------------------===//
# REQUIRES: esi-runtime, esi-cosim, rtl-sim, esitester
# RUN: rm -rf %t
# RUN: mkdir %t && cd %t
# RUN: %PYTHON% %s %t 2>&1
# RUN: esi-cosim.py -- esitester cosim env | FileCheck %s
import pycde
from pycde import AppID, Clock, Module, Reset, generator
from pycde.bsp import cosim
from pycde.constructs import Wire
from pycde.esi import CallService
from pycde.types import Bits, Channel, UInt
import sys
class PrintfExample(Module):
"""Call a printf function on the host once at startup."""
clk = Clock()
rst = Reset()
@generator
def construct(ports):
# CHECK: PrintfExample: 7
arg_data = UInt(32)(7)
sent_signal = Wire(Bits(1), "sent_signal")
sent = Bits(1)(1).reg(ports.clk,
ports.rst,
ce=sent_signal,
rst_value=Bits(1)(0))
arg_valid = ~sent & ~ports.rst
arg_chan, arg_ready = Channel(UInt(32)).wrap(arg_data, arg_valid)
sent_signal.assign(arg_ready & arg_valid)
CallService.call(AppID("PrintfExample"), arg_chan, Bits(0))
class EsiTesterTop(Module):
clk = Clock()
rst = Reset()
@generator
def construct(ports):
PrintfExample(clk=ports.clk, rst=ports.rst)
if __name__ == "__main__":
s = pycde.System(cosim.CosimBSP(EsiTesterTop),
name="EsiTester",
output_directory=sys.argv[1])
s.compile()
s.package()

View File

@ -152,6 +152,7 @@ if ieee_sims and ieee_sims[-1][1] == config.iverilog_path:
# Enable ESI runtime tests.
if config.esi_runtime == "ON":
config.available_features.add('esi-runtime')
config.available_features.add('esitester')
llvm_config.with_environment('PYTHONPATH',
[f"{config.esi_runtime_path}/python/"],

View File

@ -148,12 +148,15 @@ def ControlReg(clk: Signal,
rst: Signal,
asserts: List[Signal],
resets: List[Signal],
name: Optional[str] = None) -> BitVectorSignal:
name: Optional[str] = None) -> BitsSignal:
"""Constructs a 'control register' and returns the output. Asserts are signals
which causes the output to go high (on the next cycle). Resets do the
opposite. If both an assert and a reset are active on the same cycle, the
assert takes priority."""
assert len(asserts) > 0
assert len(resets) > 0
@modparams
def ControlReg(num_asserts: int, num_resets: int):

View File

@ -507,6 +507,45 @@ class _FuncService(ServiceDecl):
FuncService = _FuncService()
class _CallService(ServiceDecl):
"""ESI standard service to request execution of a function."""
def __init__(self):
super().__init__(self.__class__)
def call(self, name: AppID, arg: ChannelSignal,
result_type: Type) -> ChannelSignal:
"""Call a function with the given argument. 'arg' must be a ChannelSignal
with the argument value."""
func_bundle = Bundle([
BundledChannel("arg", ChannelDirection.FROM, arg.type),
BundledChannel("result", ChannelDirection.TO, result_type)
])
call_bundle = self.get(name, func_bundle)
bundle_rets = call_bundle.unpack(arg=arg)
return bundle_rets['result']
def get(self, name: AppID, func_type: Bundle) -> BundleSignal:
"""Expose a bundle to the host as a function. Bundle _must_ have 'arg' and
'result' channels going FROM the server and TO the server, respectively."""
self._materialize_service_decl()
func_call = _FromCirctValue(
raw_esi.RequestConnectionOp(
func_type._type,
hw.InnerRefAttr.get(self.symbol, ir.StringAttr.get("call")),
name._appid).toClient)
assert isinstance(func_call, BundleSignal)
return func_call
@staticmethod
def _op(sym_name: ir.StringAttr):
return raw_esi.CallServiceDeclOp(sym_name)
CallService = _CallService()
def package(sys: System):
"""Package all ESI collateral."""

View File

@ -400,7 +400,7 @@ class ModuleLikeBuilderBase(_PyProxy):
def __init__(self, builder: ModuleLikeBuilderBase, ports: PortProxyBase, ip,
loc: ir.Location) -> None:
self.bc = _BlockContext()
self.bb = BackedgeBuilder()
self.bb = BackedgeBuilder(builder.name)
self.ip = ir.InsertionPoint(ip)
self.loc = loc
self.clk = None

View File

@ -67,6 +67,22 @@ def FuncServiceDeclOp : ESI_Op<"service.std.func",
}];
}
def CallServiceDeclOp : ESI_Op<"service.std.call",
[SingleBlock, NoTerminator, HasParent<"::mlir::ModuleOp">,
Symbol, DeclareOpInterfaceMethods<ServiceDeclOpInterface>]> {
let summary = "Service against which hardware can call into software";
let arguments = (ins SymbolNameAttr:$sym_name);
let assemblyFormat = [{
$sym_name attr-dict
}];
let extraClassDeclaration = [{
std::optional<StringRef> getTypeName() { return "esi.service.std.call"; }
}];
}
def MMIOServiceDeclOp: ESI_Op<"service.std.mmio",
[HasParent<"::mlir::ModuleOp">, Symbol,
DeclareOpInterfaceMethods<ServiceDeclOpInterface>]> {

View File

@ -32,6 +32,7 @@ if (ESI_RUNTIME)
ESIRuntime
ESIPythonRuntime
esiquery
esitester
)
# If ESI Cosim is available to build then enable its tests.

View File

@ -0,0 +1,28 @@
// REQUIRES: esi-cosim, esi-runtime, rtl-sim, esitester
// RUN: rm -rf %t6 && mkdir %t6 && cd %t6
// RUN: circt-opt %s --esi-connect-services --esi-appid-hier=top=top --esi-build-manifest=top=top --esi-clean-metadata > %t4.mlir
// RUN: circt-opt %t4.mlir --lower-esi-to-physical --lower-esi-bundles --lower-esi-ports --lower-esi-to-hw=platform=cosim --lower-seq-to-sv --lower-hwarith-to-hw --canonicalize --export-split-verilog -o %t3.mlir
// RUN: cd ..
// RUN: esi-cosim.py --source %t6 --top top -- esitester cosim env | FileCheck %s
hw.module @EsiTesterTop(in %clk : !seq.clock, in %rst : i1) {
hw.instance "PrintfExample" sym @PrintfExample @PrintfExample(clk: %clk: !seq.clock, rst: %rst: i1) -> ()
}
// CHECK: PrintfExample: 7
hw.module @PrintfExample(in %clk : !seq.clock, in %rst : i1) {
%0 = hwarith.constant 7 : ui32
%true = hw.constant true
%false = hw.constant false
%1 = seq.compreg.ce %true, %clk, %5 reset %rst, %false : i1
%true_0 = hw.constant true
%2 = comb.xor bin %1, %true_0 : i1
%true_1 = hw.constant true
%3 = comb.xor bin %rst, %true_1 {sv.namehint = "inv_rst"} : i1
%4 = comb.and bin %2, %3 : i1
%chanOutput, %ready = esi.wrap.vr %0, %4 : ui32
%5 = comb.and bin %ready, %4 {sv.namehint = "sent_signal"} : i1
%6 = esi.service.req <@_CallService::@call>(#esi.appid<"PrintfExample">) : !esi.bundle<[!esi.channel<ui32> from "arg", !esi.channel<i0> to "result"]>
%result = esi.bundle.unpack %chanOutput from %6 : !esi.bundle<[!esi.channel<ui32> from "arg", !esi.channel<i0> to "result"]>
}
esi.service.std.call @_CallService

View File

@ -170,6 +170,7 @@ if ieee_sims and ieee_sims[-1][1] == config.iverilog_path:
if config.esi_runtime == "1":
config.available_features.add('esi-runtime')
tools.append('esiquery')
tools.append('esitester')
llvm_config.with_environment('PYTHONPATH',
[f"{config.esi_runtime_path}/python/"],

View File

@ -65,6 +65,19 @@ void RandomAccessMemoryDeclOp::getPortList(
ports.push_back(readPortInfo());
}
void CallServiceDeclOp::getPortList(SmallVectorImpl<ServicePortInfo> &ports) {
auto *ctxt = getContext();
ports.push_back(ServicePortInfo{
hw::InnerRefAttr::get(getSymNameAttr(), StringAttr::get(ctxt, "call")),
ChannelBundleType::get(
ctxt,
{BundledChannel{StringAttr::get(ctxt, "arg"), ChannelDirection::from,
ChannelType::get(ctxt, AnyType::get(ctxt))},
BundledChannel{StringAttr::get(ctxt, "result"), ChannelDirection::to,
ChannelType::get(ctxt, AnyType::get(ctxt))}},
/*resettable=*/UnitAttr())});
}
void FuncServiceDeclOp::getPortList(SmallVectorImpl<ServicePortInfo> &ports) {
auto *ctxt = getContext();
ports.push_back(ServicePortInfo{

View File

@ -231,6 +231,13 @@ install(TARGETS esiquery
COMPONENT ESIRuntime
)
# The esitester tool is both an example and test driver. As it is not intended
# for production use, it is not installed.
add_executable(esitester
${CMAKE_CURRENT_SOURCE_DIR}/cpp/tools/esitester.cpp
)
target_link_libraries(esitester PRIVATE ESIRuntime)
# Global variable for the path to the ESI runtime for use by tests.
set(ESIRuntimePath "${CMAKE_CURRENT_BINARY_DIR}"
CACHE INTERNAL "Path to ESI runtime" FORCE)

View File

@ -34,6 +34,8 @@
#include <typeinfo>
namespace esi {
// Forward declarations.
class AcceleratorServiceThread;
//===----------------------------------------------------------------------===//
// Constants used by low-level APIs.
@ -74,16 +76,20 @@ public:
/// subclasses.
class AcceleratorConnection {
public:
AcceleratorConnection(Context &ctxt) : ctxt(ctxt) {}
AcceleratorConnection(Context &ctxt);
virtual ~AcceleratorConnection() = default;
Context &getCtxt() const { return ctxt; }
/// Disconnect from the accelerator cleanly.
void disconnect();
/// Request the host side channel ports for a particular instance (identified
/// by the AppID path). For convenience, provide the bundle type.
virtual std::map<std::string, ChannelPort &>
requestChannelsFor(AppIDPath, const BundleType *) = 0;
AcceleratorServiceThread *getServiceThread() { return serviceThread.get(); }
using Service = services::Service;
/// Get a typed reference to a particular service type. Caller does *not* take
/// ownership of the returned pointer -- the Accelerator object owns it.
@ -112,13 +118,15 @@ protected:
const HWClientDetails &clients) = 0;
private:
/// ESI accelerator context.
Context &ctxt;
/// Cache services via a unique_ptr so they get free'd automatically when
/// Accelerator objects get deconstructed.
using ServiceCacheKey = std::tuple<const std::type_info *, AppIDPath>;
std::map<ServiceCacheKey, std::unique_ptr<Service>> serviceCache;
/// ESI accelerator context.
Context &ctxt;
std::unique_ptr<AcceleratorServiceThread> serviceThread;
};
namespace registry {
@ -149,6 +157,27 @@ struct RegisterAccelerator {
} // namespace internal
} // namespace registry
/// Background thread which services various requests. Currently, it listens on
/// ports and calls callbacks for incoming messages on said ports.
class AcceleratorServiceThread {
public:
AcceleratorServiceThread();
~AcceleratorServiceThread();
/// When there's data on any of the listenPorts, call the callback. Callable
/// from any thread.
void
addListener(std::initializer_list<ReadChannelPort *> listenPorts,
std::function<void(ReadChannelPort *, MessageData)> callback);
/// Instruct the service thread to stop running.
void stop();
private:
struct Impl;
std::unique_ptr<Impl> impl;
};
} // namespace esi
#endif // ESI_ACCELERATOR_H

View File

@ -20,6 +20,7 @@
#include <cstdint>
#include <map>
#include <optional>
#include <stdexcept>
#include <string>
#include <vector>
@ -94,6 +95,24 @@ public:
/// Get the size of the data in bytes.
size_t getSize() const { return data.size(); }
/// Cast to a type. Throws if the size of the data does not match the size of
/// the message. The lifetime of the resulting pointer is tied to the lifetime
/// of this object.
template <typename T>
const T *as() const {
if (data.size() != sizeof(T))
throw std::runtime_error("Data size does not match type size. Size is " +
std::to_string(data.size()) + ", expected " +
std::to_string(sizeof(T)) + ".");
return reinterpret_cast<const T *>(data.data());
}
/// Cast from a type to its raw bytes.
template <typename T>
static MessageData from(T &t) {
return MessageData(reinterpret_cast<const uint8_t *>(&t), sizeof(T));
}
private:
std::vector<uint8_t> data;
};

View File

@ -96,6 +96,15 @@ public:
return channels;
}
/// Cast this Bundle port to a subclass which is actually useful. Returns
/// nullptr if the cast fails.
// TODO: this probably shouldn't be 'const', but bundle ports' user access are
// const. Change that.
template <typename T>
T *getAs() const {
return const_cast<T *>(dynamic_cast<const T *>(this));
}
private:
AppID id;
std::map<std::string, ChannelPort &> channels;

View File

@ -116,8 +116,9 @@ private:
/// Service for calling functions.
class FuncService : public Service {
public:
FuncService(AcceleratorConnection *acc, AppIDPath id, std::string implName,
ServiceImplDetails details, HWClientDetails clients);
FuncService(AcceleratorConnection *acc, AppIDPath id,
const std::string &implName, ServiceImplDetails details,
HWClientDetails clients);
virtual std::string getServiceSymbol() const override;
virtual ServicePort *getPort(AppIDPath id, const BundleType *type,
@ -142,6 +143,36 @@ private:
std::string symbol;
};
/// Service for servicing function calls from the accelerator.
class CallService : public Service {
public:
CallService(AcceleratorConnection *acc, AppIDPath id, std::string implName,
ServiceImplDetails details, HWClientDetails clients);
virtual std::string getServiceSymbol() const override;
virtual ServicePort *getPort(AppIDPath id, const BundleType *type,
const std::map<std::string, ChannelPort &> &,
AcceleratorConnection &) const override;
/// A function call which gets attached to a service port.
class Callback : public ServicePort {
friend class CallService;
Callback(AcceleratorConnection &acc, AppID id,
const std::map<std::string, ChannelPort &> &channels);
public:
void connect(std::function<MessageData(const MessageData &)> callback);
private:
ReadChannelPort &arg;
WriteChannelPort &result;
AcceleratorConnection &acc;
};
private:
std::string symbol;
};
/// Registry of services which can be instantiated directly by the Accelerator
/// class if the backend doesn't do anything special with a service.
class ServiceRegistry {

View File

@ -14,6 +14,7 @@
#include "esi/Accelerator.h"
#include <cassert>
#include <map>
#include <stdexcept>
@ -23,6 +24,8 @@ using namespace esi;
using namespace esi::services;
namespace esi {
AcceleratorConnection::AcceleratorConnection(Context &ctxt)
: ctxt(ctxt), serviceThread(make_unique<AcceleratorServiceThread>()) {}
services::Service *AcceleratorConnection::getService(Service::Type svcType,
AppIDPath id,
@ -62,4 +65,105 @@ unique_ptr<AcceleratorConnection> connect(Context &ctxt, string backend,
}
} // namespace registry
struct AcceleratorServiceThread::Impl {
Impl() {}
void start() { me = std::thread(&Impl::loop, this); }
void stop() {
shutdown = true;
me.join();
}
/// When there's data on any of the listenPorts, call the callback. This
/// method can be called from any thread.
void
addListener(std::initializer_list<ReadChannelPort *> listenPorts,
std::function<void(ReadChannelPort *, MessageData)> callback);
private:
void loop();
volatile bool shutdown = false;
std::thread me;
// Protect the listeners map.
std::mutex listenerMutex;
// Map of read ports to callbacks.
std::map<ReadChannelPort *,
std::function<void(ReadChannelPort *, MessageData)>>
listeners;
};
void AcceleratorServiceThread::Impl::loop() {
// These two variables should logically be in the loop, but this avoids
// reconstructing them on each iteration.
std::vector<std::tuple<ReadChannelPort *,
std::function<void(ReadChannelPort *, MessageData)>,
MessageData>>
portUnlockWorkList;
MessageData data;
while (!shutdown) {
// Ideally we'd have some wake notification here, but this sufficies for
// now.
// TODO: investigate better ways to do this.
std::this_thread::sleep_for(std::chrono::microseconds(100));
// Check and gather data from all the read ports we are monitoring. Put the
// callbacks to be called later so we can release the lock.
{
std::lock_guard<std::mutex> g(listenerMutex);
for (auto &[channel, cb] : listeners) {
assert(channel && "Null channel in listener list");
if (channel->read(data))
portUnlockWorkList.emplace_back(channel, cb, std::move(data));
}
}
// Call the callbacks outside the lock.
for (auto [channel, cb, data] : portUnlockWorkList)
cb(channel, std::move(data));
// Clear the worklist for the next iteration.
portUnlockWorkList.clear();
}
}
void AcceleratorServiceThread::Impl::addListener(
std::initializer_list<ReadChannelPort *> listenPorts,
std::function<void(ReadChannelPort *, MessageData)> callback) {
std::lock_guard<std::mutex> g(listenerMutex);
for (auto port : listenPorts) {
if (listeners.count(port))
throw runtime_error("Port already has a listener");
listeners[port] = callback;
}
}
} // namespace esi
AcceleratorServiceThread::AcceleratorServiceThread()
: impl(std::make_unique<Impl>()) {
impl->start();
}
AcceleratorServiceThread::~AcceleratorServiceThread() { stop(); }
void AcceleratorServiceThread::stop() {
if (impl) {
impl->stop();
impl.reset();
}
}
/// When there's data on any of the listenPorts, call the callback.
void AcceleratorServiceThread::addListener(
std::initializer_list<ReadChannelPort *> listenPorts,
std::function<void(ReadChannelPort *, MessageData)> callback) {
assert(impl && "Service thread not running");
impl->addListener(listenPorts, callback);
}
void AcceleratorConnection::disconnect() {
if (serviceThread) {
serviceThread->stop();
serviceThread.reset();
}
}

View File

@ -83,8 +83,8 @@ CustomService::CustomService(AppIDPath idPath,
}
FuncService::FuncService(AcceleratorConnection *acc, AppIDPath idPath,
std::string implName, ServiceImplDetails details,
HWClientDetails clients) {
const std::string &implName,
ServiceImplDetails details, HWClientDetails clients) {
if (auto f = details.find("service"); f != details.end())
// Strip off initial '@'.
symbol = any_cast<string>(f->second).substr(1);
@ -104,8 +104,7 @@ FuncService::Function::Function(
: ServicePort(id, channels),
arg(dynamic_cast<WriteChannelPort &>(channels.at("arg"))),
result(dynamic_cast<ReadChannelPort &>(channels.at("result"))) {
if (channels.size() != 2)
throw runtime_error("FuncService must have exactly two channels");
assert(channels.size() == 2 && "FuncService must have exactly two channels");
}
void FuncService::Function::connect() {
@ -119,6 +118,45 @@ FuncService::Function::call(const MessageData &argData) {
return result.readAsync();
}
CallService::CallService(AcceleratorConnection *acc, AppIDPath idPath,
std::string implName, ServiceImplDetails details,
HWClientDetails clients) {
if (auto f = details.find("service"); f != details.end())
// Strip off initial '@'.
symbol = any_cast<string>(f->second).substr(1);
}
std::string CallService::getServiceSymbol() const { return symbol; }
ServicePort *
CallService::getPort(AppIDPath id, const BundleType *type,
const std::map<std::string, ChannelPort &> &channels,
AcceleratorConnection &acc) const {
return new Callback(acc, id.back(), channels);
}
CallService::Callback::Callback(
AcceleratorConnection &acc, AppID id,
const std::map<std::string, ChannelPort &> &channels)
: ServicePort(id, channels),
arg(dynamic_cast<ReadChannelPort &>(channels.at("arg"))),
result(dynamic_cast<WriteChannelPort &>(channels.at("result"))),
acc(acc) {
if (channels.size() != 2)
throw runtime_error("CallService must have exactly two channels");
}
void CallService::Callback::connect(
std::function<MessageData(const MessageData &)> callback) {
arg.connect();
result.connect();
acc.getServiceThread()->addListener(
{&arg}, [this, callback](ReadChannelPort *, MessageData argMsg) -> void {
MessageData resultMsg = callback(std::move(argMsg));
this->result.write(std::move(resultMsg));
});
}
Service *ServiceRegistry::createService(AcceleratorConnection *acc,
Service::Type svcType, AppIDPath id,
std::string implName,
@ -127,6 +165,8 @@ Service *ServiceRegistry::createService(AcceleratorConnection *acc,
// TODO: Add a proper registration mechanism.
if (svcType == typeid(FuncService))
return new FuncService(acc, id, implName, details, clients);
if (svcType == typeid(CallService))
return new CallService(acc, id, implName, details, clients);
return nullptr;
}
@ -134,5 +174,7 @@ Service::Type ServiceRegistry::lookupServiceType(const std::string &svcName) {
// TODO: Add a proper registration mechanism.
if (svcName == "esi.service.std.func")
return typeid(FuncService);
if (svcName == "esi.service.std.call")
return typeid(CallService);
return typeid(CustomService);
}

View File

@ -0,0 +1,86 @@
//===- esitester.cpp - ESI accelerator test/example tool ------------------===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
//
// DO NOT EDIT!
// This file is distributed as part of an ESI runtime package. The source for
// this file should always be modified within CIRCT
// (lib/dialect/ESI/runtime/cpp/tools/esitester.cpp).
//
//===----------------------------------------------------------------------===//
//
// This application isn't a utility so much as a test driver for an ESI system.
// It is also useful as an example of how to use the ESI C++ API. esiquery.cpp
// is also useful as an example.
//
//===----------------------------------------------------------------------===//
#include "esi/Accelerator.h"
#include "esi/Manifest.h"
#include "esi/Services.h"
#include <iostream>
#include <map>
#include <stdexcept>
using namespace std;
using namespace esi;
static void registerCallbacks(Accelerator *);
int main(int argc, const char *argv[]) {
// TODO: find a command line parser library rather than doing this by hand.
if (argc < 3) {
cerr << "Expected usage: " << argv[0]
<< " <backend> <connection specifier> [command]" << endl;
return -1;
}
const char *backend = argv[1];
const char *conn = argv[2];
string cmd;
if (argc > 3)
cmd = argv[3];
try {
Context ctxt;
unique_ptr<AcceleratorConnection> acc = ctxt.connect(backend, conn);
const auto &info = *acc->getService<services::SysInfo>();
Manifest manifest(ctxt, info.getJsonManifest());
std::unique_ptr<Accelerator> accel = manifest.buildAccelerator(*acc);
registerCallbacks(accel.get());
if (cmd == "loop") {
while (true) {
this_thread::sleep_for(chrono::milliseconds(100));
}
}
acc->disconnect();
cerr << "Exiting successfully\n";
return 0;
} catch (exception &e) {
cerr << "Error: " << e.what() << endl;
return -1;
}
}
void registerCallbacks(Accelerator *accel) {
auto ports = accel->getPorts();
auto f = ports.find(AppID("PrintfExample"));
if (f != ports.end()) {
auto callPort = f->second.getAs<services::CallService::Callback>();
if (callPort)
callPort->connect([](const MessageData &data) -> MessageData {
cout << "PrintfExample: " << *data.as<uint32_t>() << endl;
return MessageData();
});
}
}