Skip to main content

HostApi

Trait HostApi 

Source
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§

Source

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.

Source

fn call(&self, op: &str, args: Vec<Value>) -> Result<Value, RuntimeError>

Invokes one operation.

Provided Methods§

Source

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.

Source

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".

Implementors§