mirror of https://github.com/llvm/circt.git
[ESI][PyCDE] Callback service (#7153)
Call software functions from hardware.
This commit is contained in:
parent
0e13467021
commit
1f6c29fb64
|
@ -19,6 +19,7 @@ else()
|
|||
CIRCTPythonModules
|
||||
ESIRuntime
|
||||
ESIPythonRuntime
|
||||
esitester
|
||||
)
|
||||
|
||||
# If ESI Cosim is available to build then enable its tests.
|
||||
|
|
|
@ -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()
|
|
@ -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/"],
|
||||
|
|
|
@ -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):
|
||||
|
||||
|
|
|
@ -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."""
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>]> {
|
||||
|
|
|
@ -32,6 +32,7 @@ if (ESI_RUNTIME)
|
|||
ESIRuntime
|
||||
ESIPythonRuntime
|
||||
esiquery
|
||||
esitester
|
||||
)
|
||||
|
||||
# If ESI Cosim is available to build then enable its tests.
|
||||
|
|
|
@ -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
|
|
@ -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/"],
|
||||
|
|
|
@ -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{
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue