///|
/// The access of a registered host function to the kernel of the plugin that calls it.
pub struct CallContext {
priv plugin : PluginState
}
///|
/// A host function that the embedder registers in a namespace that it chooses.
pub struct HostFunction {
priv namespace_ : Namespace
priv name : FunctionName
priv signature : @engine.Signature
priv function : (CallContext, Array[@engine.Value]) -> Array[@engine.Value] raise @engine.HostError
}
///|
pub fn HostFunction::new(
namespace_ : Namespace,
name : FunctionName,
signature : @engine.Signature,
function : (CallContext, Array[@engine.Value]) -> Array[@engine.Value] raise @engine.HostError,
) -> HostFunction {
{ namespace_, name, signature, function, }
}
///|
/// `namespace.name`.
pub fn HostFunction::label(self : HostFunction) -> String {
"\{self.namespace_.text()}.\{self.name.text()}"
}
///|
/// A manifest that the library refuses: the import that a registration names, and why.
#derive(Eq, Debug)
pub(all) suberror ManifestError {
/// The name is a function that the library links in `extism:host/env`.
ReservedName(String)
/// The embedder registered the name before.
AlreadyRegistered(String)
}
///|
pub extend ManifestError with Eq::{equal, not_equal}
///|
pub extend ManifestError with @debug.Debug::{to_repr}
///|
/// The description of one plugin: its module bytes, its limits, its policy, the host functions that
/// the embedder registers and, when it is enabled, the WASI sandbox. The builder steps give a new
/// manifest; the library reads nothing from a file, a URL or the environment.
pub struct Manifest {
priv module_ : Bytes
priv limits : Limits
priv policy : Policy
priv functions : Array[HostFunction]
priv sandbox : @wasi.Sandbox?
}
///|
/// A manifest with the module bytes, the default limits, the default policy, no registered host
/// function and no WASI sandbox.
pub fn Manifest::new(module_ : Bytes) -> Manifest {
{
module_,
limits: Limits::default(),
policy: Policy::default(),
functions: [],
sandbox: None,
}
}
///|
pub fn Manifest::limits(self : Manifest, limits : Limits) -> Manifest {
{ ..self, limits, }
}
///|
pub fn Manifest::policy(self : Manifest, policy : Policy) -> Manifest {
{ ..self, policy, }
}
///|
/// The manifest with the WASI preview 1 sandbox enabled.
pub fn Manifest::wasi(self : Manifest, sandbox : @wasi.Sandbox) -> Manifest {
{ ..self, sandbox: Some(sandbox), }
}
///|
/// The manifest with `function` registered. A name in `extism:host/env` and a name that is already
/// registered are refused.
pub fn Manifest::register(
self : Manifest,
function : HostFunction,
) -> Manifest raise ManifestError {
if function.namespace_.text() == @kernel.import_namespace {
raise ReservedName(function.label())
}
if self.functions.iter().any(f => f.label() == function.label()) {
raise AlreadyRegistered(function.label())
}
{ ..self, functions: [..self.functions, function], }
}
///|
/// The Extism host functions that the library links in `extism:host/env`, with their upstream
/// signatures.
#derive(Eq, Debug)
pub(all) enum ExtismFunction {
ConfigGet
VarGet
VarSet
LogTrace
LogDebug
LogInfo
LogWarn
LogError
GetLogLevel
HttpRequest
HttpStatusCode
HttpHeaders
}
///|
pub extend ExtismFunction with Eq::{equal, not_equal}
///|
pub extend ExtismFunction with @debug.Debug::{to_repr}
///|
pub fn ExtismFunction::all() -> Array[ExtismFunction] {
[
ConfigGet,
VarGet,
VarSet,
LogTrace,
LogDebug,
LogInfo,
LogWarn,
LogError,
GetLogLevel,
HttpRequest,
HttpStatusCode,
HttpHeaders,
]
}
///|
pub fn ExtismFunction::name(self : ExtismFunction) -> String {
match self {
ConfigGet => "config_get"
VarGet => "var_get"
VarSet => "var_set"
LogTrace => "log_trace"
LogDebug => "log_debug"
LogInfo => "log_info"
LogWarn => "log_warn"
LogError => "log_error"
GetLogLevel => "get_log_level"
HttpRequest => "http_request"
HttpStatusCode => "http_status_code"
HttpHeaders => "http_headers"
}
}
///|
pub fn ExtismFunction::from_name(name : StringView) -> ExtismFunction? {
ExtismFunction::all().iter().find_first(f => f.name() == name.to_owned())
}
///|
pub fn ExtismFunction::signature(self : ExtismFunction) -> @engine.Signature {
match self {
ConfigGet | VarGet => @engine.Signature::new([I64], [I64])
VarSet => @engine.Signature::new([I64, I64], [])
LogTrace | LogDebug | LogInfo | LogWarn | LogError =>
@engine.Signature::new([I64], [])
GetLogLevel | HttpStatusCode => @engine.Signature::new([], [I32])
HttpRequest => @engine.Signature::new([I64, I64], [I64])
HttpHeaders => @engine.Signature::new([], [I64])
}
}
///|
/// What links one import of a plugin.
priv enum Link {
Kernel(@kernel.KernelFunction)
Extism(ExtismFunction)
Registered(HostFunction)
Wasi(@wasi.Function)
}
///|
/// A plugin that the library refuses to load.
#derive(Debug)
pub(all) suberror LoadError {
/// Nothing links this import: the module and the field.
UnlinkedImport(String, String)
/// The import has a signature other than the one that the library links: the import and the
/// detail.
WrongSignature(String, String)
/// The engine refuses the module or its instantiation.
Engine(@engine.EngineError)
}
///|
pub extend LoadError with @debug.Debug::{to_repr}
///|
fn wasi_signature(function : @wasi.Function) -> @engine.Signature {
let convert = (types : Array[@wasi.WasiType]) => {
types.map(t => {
match t {
I32 => @engine.ValueType::I32
I64 => I64
}
})
}
@engine.Signature::new(
convert(function.params()),
convert(function.results()),
)
}
///|
fn kernel_signature(function : @kernel.KernelFunction) -> @engine.Signature {
let convert = (types : Array[@kernel.KernelType]) => {
types.map(t => {
match t {
I32 => @engine.ValueType::I32
I64 => I64
}
})
}
@engine.Signature::new(
convert(function.params()),
convert(function.results()),
)
}
///|
/// What links each import, in import order. This is the pure import check of a load: the first import
/// that nothing links refuses the load and is named.
fn Manifest::links(
self : Manifest,
imports : Array[@engine.ImportDescriptor],
) -> Array[Link] raise LoadError {
imports.map(i => {
let link : Link = if i.module_ == @kernel.import_namespace &&
@kernel.KernelFunction::from_name(i.name) is Some(function) {
Kernel(function)
} else if i.module_ == @kernel.import_namespace &&
ExtismFunction::from_name(i.name) is Some(function) {
Extism(function)
} else if self.functions
.iter()
.find_first(f => {
f.namespace_.text() == i.module_ && f.name.text() == i.name
})
is Some(function) {
Registered(function)
} else if self.sandbox is Some(_) &&
i.module_ == @wasi.import_namespace &&
@wasi.Function::from_name(i.name) is Some(function) {
Wasi(function)
} else {
raise UnlinkedImport(i.module_, i.name)
}
let wanted = match link {
Kernel(function) => kernel_signature(function)
Extism(function) => function.signature()
Registered(function) => function.signature
Wasi(function) => wasi_signature(function)
}
guard i.kind is Function(Some(given)) && given == wanted else {
raise WrongSignature(
"\{i.module_}.\{i.name}",
"the library links it as \{wanted}",
)
}
link
})
}