pub trait HostApi: Send + Sync {
// Required methods
fn module_schema(&self) -> ModuleSchema;
fn call(&self, op: &str, args: Vec<Value>) -> Result<Value, RuntimeError>;
// Provided methods
fn call_with(
&self,
op: &str,
args: Vec<Value>,
back: &mut dyn Reentry,
) -> Result<Value, RuntimeError> { ... }
fn call_resource(
&self,
handle: &ResourceHandle,
op: &str,
args: Vec<Value>,
back: &mut dyn Reentry,
) -> Result<Value, RuntimeError> { ... }
}Expand description
One host-provided module, such as console or env.
A host is shared by every task of a run, so an operation is invoked
through a shared reference and a host is Send + Sync. A host that needs
mutable state of its own says so with a lock it owns, which is also what
decides how much of it two tasks may do at once: console serializes its
writes so a line is never torn, while clock.sleep holds nothing, so two
tasks can wait at the same time instead of queueing behind each other.
§An operation that blocks
A host call is a hole in the run’s safepoint chain. The interpreter checks
fuel, the deadline, and cancellation at loop back edges, calls, and
await, and a program sitting inside a host reaches none of the three;
Budget::charge_host_call checks the deadline and the cancellation flag
once more before dispatch, but that bounds when a call starts, not how
long it runs. Nothing in the runtime can interrupt a host that is waiting
in accept or read. So this is a contract the boundary states and each
host keeps, rather than something the boundary can enforce.
An operation that waits must bound how long it waits. It polls in steps
short enough that the run’s controls are still responsive, asks the
Reentry it was handed whether the run has been stopped
(Reentry::is_cancelled) and how long it has left
(Reentry::time_left) between steps, and holds no lock while it does —
a host waiting under its own mutex blocks every other task that wants it.
One total allowance covers a multi-part operation: a per-read timeout that
starts again on every successful read bounds nothing, because a peer that
makes slow progress can hold the call open forever. http.Server.handle
is the worked example. It accepts by polling rather than blocking, gives
the whole of one request a single deadline clamped by what the run has
left, and answers “nothing more to serve” when the run is stopped, so the
program’s own loop ends and the budget reports the stop it owns.
An operation that genuinely cannot cooperate — a C library call with no timeout, a syscall that cannot be interrupted — must say so in its own documentation, so an embedder knows the run’s deadline does not bound that call and can decide what to do about it. What is not acceptable is a host that blocks indefinitely and says nothing.
§Migrating from the five-accessor form
This trait used to ask a module to describe itself five times — name(),
capability(), schema(), types(), and resources(). It asks once
now, through HostApi::module_schema, and the five are gone rather than
defaulted: a defaulted accessor is an overridable one, and an overridable
one is a second description of the module, which the checker and the
boundary could then read differently. An implementation written against
the old shape fails to compile with not all trait items implemented,
which is the intended way to find out.
The migration is to delete all five and write the table they were reading from:
const COMPANY: ModuleSchema = ModuleSchema {
name: "company",
capability: "directory",
operations: &[],
types: &[],
resources: &[],
};
impl HostApi for Company {
fn module_schema(&self) -> ModuleSchema {
COMPANY
}
fn call(&self, op: &str, args: Vec<Value>) -> Result<Value, RuntimeError> {
// unchanged
}
}name is the string name() returned, capability is the string behind
the Capability capability() returned, and the three slices are what
schema(), types(), and resources() returned. call, call_with,
and call_resource are untouched.
The one thing that gets harder is a schema assembled at run time. The old
accessors handed back borrows of self, so a host could keep a String
and a Vec<OperationSchema> and return references into itself;
ModuleSchema is Copy with 'static contents, so a module whose
shape comes from configuration or a manifest builds its table once, leaks
it, and hands out the same copy.
Putting a lifetime on ModuleSchema would not give the old form back.
A module registered here is a Box<dyn HostApi>, which is
Box<dyn HostApi + 'static> — a spawned task’s thread holds the registry
and std::thread::Builder::spawn takes a 'static closure — so a host
has nothing outside itself to borrow a schema from, and borrowing from a
field beside another one of its own is a self-referential struct.
ModuleSchema’s own documentation weighs that against the alternatives
and spells the pattern out; crates/cove-runtime/tests/embedding.rs runs
it end to end.
Required Methods§
Sourcefn module_schema(&self) -> ModuleSchema
fn module_schema(&self) -> ModuleSchema
The whole of what this module declares about itself: the name Cove source uses, the capability a host must grant for it, the operations it exposes, the types it declares, and the kinds of resource it can open.
The schema is the module’s declaration of itself: a host cannot
expose an operation without saying what it takes, what it produces,
what it costs the outside world, and whether its result may cross a
task boundary. The boundary holds every call to it, so an operation
arriving in call has already been checked against what is declared
here.
One table rather than five methods, because this exact value is also
what the checker reads: cove_sema::Compiler::with_host_schema
takes a ModuleSchema, so the description a run enforces and the
description cove check checked a call against are the same bytes
for an embedder’s module as they already are for a shipped one. This
is the only way to ask a module what it declares — there is nothing
else on this trait to override instead, so a module cannot describe
itself one way to the boundary and another way to the checker.
Provided Methods§
Sourcefn call_with(
&self,
op: &str,
args: Vec<Value>,
back: &mut dyn Reentry,
) -> Result<Value, RuntimeError>
fn call_with( &self, op: &str, args: Vec<Value>, back: &mut dyn Reentry, ) -> Result<Value, RuntimeError>
Invokes one operation.
The default forwards to HostApi::call, which is what a module that
never runs a Cove callback wants. A module that does — clock.every,
http.Server.handle — overrides this instead and leaves call
unreachable.
back is the way into the program that made this call, and it is on
loan for the duration of the call and no longer. Reentry states
the whole of what may be done with it, and the parts an implementor is
most likely to get wrong are these: it may not be retained past this
return, it may be used as many times as the operation means, it may be
used from this thread only, and no lock this module owns may be held
while it is used, because the Cove code it runs may call this module
again.
Sourcefn call_resource(
&self,
handle: &ResourceHandle,
op: &str,
args: Vec<Value>,
back: &mut dyn Reentry,
) -> Result<Value, RuntimeError>
fn call_resource( &self, handle: &ResourceHandle, op: &str, args: Vec<Value>, back: &mut dyn Reentry, ) -> Result<Value, RuntimeError>
Invokes one operation on a handle this module issued.
A module that declares no resources can never be reached here, so the default says so rather than inventing an answer.
back is the same loan HostApi::call_with describes, under the
same rules, and the lock rule is sharper here than anywhere else: the
table of open resources is exactly the lock a module holds, and the
callback is exactly the code that may ask for a resource in it. Take
what the callback’s work needs, release the guard, and reenter after.
Dyn Compatibility§
This trait is dyn compatible.
In older versions of Rust, dyn compatibility was called "object safety".