mirror of
https://github.com/EasyTier/EasyTier.git
synced 2026-10-10 12:46:14 -08:00
Overhaul the WASI guest data plane for throughput and add the host capabilities it relies on. The externally driven Tokio runtime now runs its timer pre-turn only when a tracked deadline has expired, and all WASI-reachable timers (STUN, port mapping, WebClient, UDP flow cleanup) go through the portable time facade so conditional timer driving cannot starve them. Data plane: - Move read/write deadlines onto TCP and UDP resources with one ABI setter per direction, reuse a single expiration timer per resource, and drop timeout arguments from the four hot data-plane submissions (ABI v3). Checked absolute instants treat unrepresentable finite timeouts as unbounded instead of panicking. - Batch host traffic: vectored TCP frame writes combine queued slices into one host operation, and reads request a bounded 64 KiB while retaining excess bytes in the stream buffer. - Complete TCP writes inside the guest with cancellation-safe writes, reporting the completed prefix before honoring cancellation or timeout so hosts never replay bytes. - Repoll smoltcp egress immediately on zero poll delay, enlarge virtual UDP receive queues to 128 KiB payload with 128 metadata slots, and bound UDP session receive buffers to 8 KiB plus one byte while keeping oversized-datagram detection. Host integration: - Add optional algorithm-neutral AEAD seal/open imports with the ring backend as fallback, and pin the ring AES-128-GCM wire vector so the Go host stays interoperable. - Forward instance events to hosts through one best-effort, synchronous, non-blocking import. - Add a repository-owned build entry point for the Go host artifact: Binaryen 131 at -O4 with cached, SHA-256-verified official archives.
793 lines
28 KiB
Rust
793 lines
28 KiB
Rust
//! Runtime implementation and lifecycle exports for a WASI core instance.
|
|
|
|
use crate::{
|
|
config::toml::TomlConfig, connectivity::connector_host::HostConnectorEnvironmentSnapshot,
|
|
};
|
|
|
|
pub(super) type WasiCore = crate::instance::CoreInstance<
|
|
crate::connectivity::connector_host::ConnectorHost<
|
|
crate::wasi::adapter::socket::backend::WasiHostSocketBackend,
|
|
crate::wasi::adapter::environment::WasiHostConnectorEnvironmentIo,
|
|
>,
|
|
>;
|
|
|
|
pub(super) struct WasiCoreRuntime {
|
|
socket_runtime: crate::host::socket::HostSocketRuntime,
|
|
core: std::sync::Arc<WasiCore>,
|
|
}
|
|
|
|
impl WasiCoreRuntime {
|
|
pub(super) fn core(&self) -> &std::sync::Arc<WasiCore> {
|
|
&self.core
|
|
}
|
|
|
|
pub(super) fn notify_host_completions(&self) {
|
|
self.socket_runtime.notify_completions();
|
|
}
|
|
}
|
|
|
|
pub(super) fn new_wasi_core_runtime(
|
|
config: TomlConfig,
|
|
process_runtime: std::sync::Arc<crate::process_runtime::CoreProcessRuntime>,
|
|
environment_snapshot: HostConnectorEnvironmentSnapshot,
|
|
packet_sink: crate::host::packet::HostPacketSinkHandle,
|
|
event_sink: u64,
|
|
) -> anyhow::Result<WasiCoreRuntime> {
|
|
use std::sync::Arc;
|
|
|
|
use crate::host::{dns::HostDnsResolver, packet::HostPacketSink, socket::HostSocketRuntime};
|
|
use crate::{
|
|
connectivity::connector_host::new_connector_host,
|
|
instance::{CoreHostAdapters, CoreInstance},
|
|
wasi::adapter::{
|
|
dns::WasiHostDnsIo, environment::WasiHostConnectorEnvironmentIo,
|
|
event::WasiHostEventSink, packet::WasiHostPacketIo,
|
|
socket::backend::WasiHostSocketBackend,
|
|
},
|
|
};
|
|
|
|
let socket_runtime = HostSocketRuntime::new();
|
|
let host = Arc::new(new_connector_host(
|
|
socket_runtime.clone(),
|
|
Arc::new(WasiHostSocketBackend::default()),
|
|
environment_snapshot,
|
|
Arc::new(WasiHostConnectorEnvironmentIo),
|
|
));
|
|
let dns = Arc::new(HostDnsResolver::new(
|
|
socket_runtime.clone(),
|
|
Arc::new(WasiHostDnsIo),
|
|
));
|
|
let packet_sink = Arc::new(HostPacketSink::new(
|
|
socket_runtime.clone(),
|
|
Arc::new(WasiHostPacketIo),
|
|
packet_sink,
|
|
));
|
|
let mut adapters = CoreHostAdapters::new(host, dns, packet_sink, process_runtime);
|
|
adapters.events = Arc::new(WasiHostEventSink::new(event_sink));
|
|
let core = CoreInstance::from_toml(config, adapters)?;
|
|
|
|
Ok(WasiCoreRuntime {
|
|
socket_runtime,
|
|
core,
|
|
})
|
|
}
|
|
|
|
mod abi {
|
|
use std::{
|
|
cell::RefCell,
|
|
collections::BTreeMap,
|
|
sync::{Arc, Mutex},
|
|
};
|
|
|
|
use tokio::{runtime::Builder, task::JoinHandle};
|
|
|
|
use crate::{
|
|
config::toml::{ConfigLoader as _, TomlConfig},
|
|
foundation::time::{clear_domain, enter_domain, next_deadline_millis},
|
|
host::packet::{HostPacket, HostPacketSinkHandle},
|
|
instance::{
|
|
CoreInstanceState,
|
|
manager::{InstanceFactory, ManagedInstance},
|
|
},
|
|
process_runtime::{CoreProcessRuntime, ProtectedTcpPortLease},
|
|
wasi::runtime_driver::{RuntimeDriveOutcome, RuntimeDriver},
|
|
};
|
|
|
|
use super::{WasiCoreRuntime, new_wasi_core_runtime};
|
|
use crate::wasi::schema::WasiCoreInstanceCreateConfig;
|
|
|
|
#[cfg(feature = "proxy-smoltcp-stack")]
|
|
mod data_plane;
|
|
|
|
const MAX_CREATE_CONFIG_LEN: usize = 16 * 1024 * 1024;
|
|
const MAX_GUEST_BUFFER_LEN: usize = MAX_CREATE_CONFIG_LEN;
|
|
const INVALID_HANDLE: i32 = -1;
|
|
const INVALID_STATE: i32 = -2;
|
|
const INVALID_INPUT: i32 = -3;
|
|
const ASYNC_ERROR: i32 = -4;
|
|
const BUSY: i32 = -5;
|
|
|
|
struct WasiAbiState {
|
|
next_handle: u64,
|
|
handles: BTreeMap<u64, WasiHandleState>,
|
|
buffers: BTreeMap<u32, Box<[u8]>>,
|
|
active_instance: bool,
|
|
global_error: String,
|
|
}
|
|
|
|
struct WasiHandleState {
|
|
instance_id: uuid::Uuid,
|
|
error: String,
|
|
}
|
|
|
|
impl Default for WasiAbiState {
|
|
fn default() -> Self {
|
|
Self {
|
|
next_handle: 0,
|
|
handles: BTreeMap::new(),
|
|
buffers: BTreeMap::new(),
|
|
active_instance: false,
|
|
global_error: String::new(),
|
|
}
|
|
}
|
|
}
|
|
|
|
struct WasiContext {
|
|
factory: WasiInstanceFactory,
|
|
instances: RefCell<BTreeMap<uuid::Uuid, Arc<WasiInstance>>>,
|
|
abi: RefCell<WasiAbiState>,
|
|
}
|
|
|
|
impl Default for WasiContext {
|
|
fn default() -> Self {
|
|
let factory = WasiInstanceFactory {
|
|
process_runtime: CoreProcessRuntime::new(),
|
|
};
|
|
Self {
|
|
factory,
|
|
instances: RefCell::new(BTreeMap::new()),
|
|
abi: RefCell::new(WasiAbiState::default()),
|
|
}
|
|
}
|
|
}
|
|
|
|
thread_local! {
|
|
static CONTEXT: WasiContext = WasiContext::default();
|
|
}
|
|
|
|
struct WasiInstanceFactory {
|
|
process_runtime: std::sync::Arc<CoreProcessRuntime>,
|
|
}
|
|
|
|
struct WasiCreateContext {
|
|
domain: u64,
|
|
environment: crate::connectivity::connector_host::HostConnectorEnvironmentSnapshot,
|
|
packet_sink: HostPacketSinkHandle,
|
|
event_sink: u64,
|
|
}
|
|
|
|
struct WasiInstance {
|
|
instance_id: uuid::Uuid,
|
|
domain: u64,
|
|
core: WasiCoreRuntime,
|
|
execution: Mutex<WasiExecution>,
|
|
_protected_tcp_port_leases: Vec<ProtectedTcpPortLease>,
|
|
}
|
|
|
|
struct WasiExecution {
|
|
runtime: tokio::runtime::Runtime,
|
|
runtime_driver: RuntimeDriver,
|
|
drive_again: bool,
|
|
start_task: Option<JoinHandle<anyhow::Result<()>>>,
|
|
stop_task: Option<JoinHandle<()>>,
|
|
}
|
|
|
|
impl ManagedInstance for WasiInstance {
|
|
fn instance_id(&self) -> uuid::Uuid {
|
|
self.instance_id
|
|
}
|
|
}
|
|
|
|
impl InstanceFactory for WasiInstanceFactory {
|
|
type Instance = WasiInstance;
|
|
type CreateContext = WasiCreateContext;
|
|
type Error = anyhow::Error;
|
|
|
|
fn create(
|
|
&self,
|
|
config: TomlConfig,
|
|
context: Self::CreateContext,
|
|
) -> Result<Arc<Self::Instance>, Self::Error> {
|
|
let instance_id = config.get_id();
|
|
let protected_tcp_ports = context
|
|
.environment
|
|
.protected_tcp_ports
|
|
.iter()
|
|
.copied()
|
|
.map(|port| self.process_runtime.protect_tcp_port(port))
|
|
.collect();
|
|
let runtime_driver = RuntimeDriver::default();
|
|
let park_driver = runtime_driver.clone();
|
|
let runtime = Builder::new_current_thread()
|
|
.enable_time()
|
|
.on_thread_park(move || park_driver.on_thread_park())
|
|
.build()?;
|
|
let core = {
|
|
let _domain = enter_domain(context.domain);
|
|
let _runtime = runtime.enter();
|
|
new_wasi_core_runtime(
|
|
config,
|
|
self.process_runtime.clone(),
|
|
context.environment,
|
|
context.packet_sink,
|
|
context.event_sink,
|
|
)?
|
|
};
|
|
|
|
Ok(Arc::new(WasiInstance {
|
|
instance_id,
|
|
domain: context.domain,
|
|
core,
|
|
execution: Mutex::new(WasiExecution {
|
|
runtime,
|
|
runtime_driver,
|
|
drive_again: false,
|
|
start_task: None,
|
|
stop_task: None,
|
|
}),
|
|
_protected_tcp_port_leases: protected_tcp_ports,
|
|
}))
|
|
}
|
|
}
|
|
|
|
impl WasiAbiState {
|
|
fn allocate_handle(&mut self) -> Result<u64, i32> {
|
|
if self.active_instance {
|
|
self.set_global_error("core instance lifecycle call is not reentrant");
|
|
return Err(BUSY);
|
|
}
|
|
loop {
|
|
self.next_handle = self.next_handle.wrapping_add(1);
|
|
if self.next_handle != 0 && !self.handles.contains_key(&self.next_handle) {
|
|
return Ok(self.next_handle);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn set_global_error(&mut self, error: impl ToString) {
|
|
self.global_error = error.to_string();
|
|
}
|
|
|
|
fn set_handle_error(&mut self, handle: u64, error: impl ToString) {
|
|
if let Some(state) = self.handles.get_mut(&handle) {
|
|
state.error = error.to_string();
|
|
} else {
|
|
self.set_global_error(error);
|
|
}
|
|
}
|
|
|
|
fn error_for_handle(&self, handle: u64) -> &str {
|
|
self.handles
|
|
.get(&handle)
|
|
.map(|state| state.error.as_str())
|
|
.unwrap_or(self.global_error.as_str())
|
|
}
|
|
|
|
fn begin_instance_call(&mut self, handle: u64) -> Result<uuid::Uuid, i32> {
|
|
if self.active_instance {
|
|
self.set_handle_error(handle, "core instance lifecycle call is not reentrant");
|
|
return Err(BUSY);
|
|
}
|
|
let Some(instance_id) = self.handles.get(&handle).map(|state| state.instance_id) else {
|
|
self.set_global_error(format!("unknown core instance handle: {handle}"));
|
|
return Err(INVALID_HANDLE);
|
|
};
|
|
self.active_instance = true;
|
|
Ok(instance_id)
|
|
}
|
|
|
|
fn begin_instance_drop(&mut self, handle: u64) -> Result<uuid::Uuid, i32> {
|
|
self.begin_instance_call(handle)
|
|
}
|
|
|
|
fn finish_instance_call(&mut self) {
|
|
debug_assert!(self.active_instance);
|
|
self.active_instance = false;
|
|
}
|
|
|
|
fn finish_instance_drop(&mut self, handle: u64) {
|
|
debug_assert!(self.active_instance);
|
|
self.handles.remove(&handle);
|
|
self.active_instance = false;
|
|
}
|
|
|
|
fn read_buffer(&self, pointer: u32, length: usize) -> anyhow::Result<Vec<u8>> {
|
|
let buffer = self
|
|
.buffers
|
|
.get(&pointer)
|
|
.ok_or_else(|| anyhow::anyhow!("unknown guest buffer: {pointer}"))?;
|
|
if length > buffer.len() {
|
|
anyhow::bail!(
|
|
"guest buffer length {length} exceeds allocation {}",
|
|
buffer.len()
|
|
);
|
|
}
|
|
Ok(buffer[..length].to_vec())
|
|
}
|
|
}
|
|
|
|
impl WasiInstance {
|
|
fn start(&self) -> anyhow::Result<()> {
|
|
let mut execution = self.execution.lock().unwrap();
|
|
if execution.start_task.is_some()
|
|
|| execution.stop_task.is_some()
|
|
|| self.core.core().state() != CoreInstanceState::Created
|
|
{
|
|
anyhow::bail!("core instance cannot schedule start from its current state");
|
|
}
|
|
let instance = self.core.core().clone();
|
|
execution.start_task = Some(
|
|
execution
|
|
.runtime
|
|
.spawn(async move { instance.start().await }),
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
fn stop(&self) {
|
|
let mut execution = self.execution.lock().unwrap();
|
|
if execution.stop_task.is_some()
|
|
|| self.core.core().state() == CoreInstanceState::Stopped
|
|
{
|
|
return;
|
|
}
|
|
let instance = self.core.core().clone();
|
|
execution.stop_task = Some(execution.runtime.spawn(async move {
|
|
instance.stop().await;
|
|
}));
|
|
}
|
|
|
|
fn drive(&self) -> anyhow::Result<()> {
|
|
let _domain = enter_domain(self.domain);
|
|
let advance_timers = next_deadline_millis(self.domain) == Some(0);
|
|
let mut execution = self.execution.lock().unwrap();
|
|
execution.drive_again = execution
|
|
.runtime_driver
|
|
.drive(&execution.runtime, advance_timers)
|
|
== RuntimeDriveOutcome::BudgetExhausted;
|
|
|
|
if execution
|
|
.start_task
|
|
.as_ref()
|
|
.is_some_and(JoinHandle::is_finished)
|
|
{
|
|
let task = execution.start_task.take().unwrap();
|
|
match execution.runtime.block_on(task) {
|
|
Ok(Ok(())) => {}
|
|
Ok(Err(error)) => return Err(error),
|
|
Err(error) => anyhow::bail!("core instance start task failed: {error}"),
|
|
}
|
|
}
|
|
if execution
|
|
.stop_task
|
|
.as_ref()
|
|
.is_some_and(JoinHandle::is_finished)
|
|
{
|
|
let task = execution.stop_task.take().unwrap();
|
|
if let Err(error) = execution.runtime.block_on(task) {
|
|
anyhow::bail!("core instance stop task failed: {error}");
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn state_code(&self) -> i32 {
|
|
let execution = self.execution.lock().unwrap();
|
|
if execution.stop_task.is_some() {
|
|
return 3;
|
|
}
|
|
if execution.start_task.is_some() {
|
|
return 1;
|
|
}
|
|
match self.core.core().state() {
|
|
CoreInstanceState::Created => 0,
|
|
CoreInstanceState::Starting => 1,
|
|
CoreInstanceState::Running => 2,
|
|
CoreInstanceState::Stopping => 3,
|
|
CoreInstanceState::Stopped => 4,
|
|
}
|
|
}
|
|
|
|
fn next_wait_millis(&self) -> Option<u64> {
|
|
let execution = self.execution.lock().unwrap();
|
|
if execution.drive_again {
|
|
Some(0)
|
|
} else {
|
|
next_deadline_millis(self.domain)
|
|
}
|
|
}
|
|
|
|
fn send_packet(&self, packet: Vec<u8>) {
|
|
let packet_plane = self.core.core().packet_plane();
|
|
self.execution.lock().unwrap().runtime.spawn(async move {
|
|
if let Err(error) = packet_plane
|
|
.send_ip_packet(HostPacket::copy_from_payload(&packet))
|
|
.await
|
|
{
|
|
tracing::warn!(?error, "host packet ingress failed");
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
fn decode_create_config(encoded: &[u8]) -> anyhow::Result<WasiCoreInstanceCreateConfig> {
|
|
if encoded.is_empty() || encoded.len() > MAX_CREATE_CONFIG_LEN {
|
|
anyhow::bail!("invalid host core instance config buffer");
|
|
}
|
|
let config: WasiCoreInstanceCreateConfig = serde_json::from_slice(encoded)?;
|
|
config.validate()?;
|
|
Ok(config)
|
|
}
|
|
|
|
fn with_instance(
|
|
handle: u64,
|
|
operation: impl FnOnce(&WasiInstance) -> anyhow::Result<i32>,
|
|
) -> i32 {
|
|
let instance_id =
|
|
match CONTEXT.with(|context| context.abi.borrow_mut().begin_instance_call(handle)) {
|
|
Ok(instance_id) => instance_id,
|
|
Err(status) => return status,
|
|
};
|
|
let instance = manager_get(instance_id);
|
|
let status = match instance {
|
|
Some(instance) => match operation(&instance) {
|
|
Ok(status) => status,
|
|
Err(error) => {
|
|
CONTEXT.with(|context| {
|
|
context.abi.borrow_mut().set_handle_error(handle, error);
|
|
});
|
|
ASYNC_ERROR
|
|
}
|
|
},
|
|
None => {
|
|
CONTEXT.with(|context| {
|
|
context.abi.borrow_mut().set_handle_error(
|
|
handle,
|
|
format!("core instance {instance_id} is not registered"),
|
|
);
|
|
});
|
|
INVALID_STATE
|
|
}
|
|
};
|
|
CONTEXT.with(|context| context.abi.borrow_mut().finish_instance_call());
|
|
status
|
|
}
|
|
|
|
fn with_abi_state<T>(operation: impl FnOnce(&WasiAbiState) -> T) -> T {
|
|
CONTEXT.with(|context| operation(&context.abi.borrow()))
|
|
}
|
|
|
|
fn with_abi_state_mut<T>(operation: impl FnOnce(&mut WasiAbiState) -> T) -> T {
|
|
CONTEXT.with(|context| operation(&mut context.abi.borrow_mut()))
|
|
}
|
|
|
|
fn set_abi_error(error: impl ToString) {
|
|
with_abi_state_mut(|state| state.set_global_error(error));
|
|
}
|
|
|
|
fn set_instance_error(handle: u64, error: impl ToString) {
|
|
with_abi_state_mut(|state| state.set_handle_error(handle, error));
|
|
}
|
|
|
|
fn manager_get(instance_id: uuid::Uuid) -> Option<std::sync::Arc<WasiInstance>> {
|
|
CONTEXT.with(|context| context.instances.borrow().get(&instance_id).cloned())
|
|
}
|
|
|
|
fn manager_remove(instance_id: uuid::Uuid) -> Option<std::sync::Arc<WasiInstance>> {
|
|
CONTEXT.with(|context| context.instances.borrow_mut().remove(&instance_id))
|
|
}
|
|
|
|
fn manager_create(
|
|
config: TomlConfig,
|
|
context: WasiCreateContext,
|
|
) -> Result<
|
|
std::sync::Arc<WasiInstance>,
|
|
crate::instance::manager::InstanceCreateError<anyhow::Error>,
|
|
> {
|
|
CONTEXT.with(|wasi| {
|
|
let instance = wasi
|
|
.factory
|
|
.create(config, context)
|
|
.map_err(crate::instance::manager::InstanceCreateError::Factory)?;
|
|
let instance_id = instance.instance_id();
|
|
let mut instances = wasi.instances.borrow_mut();
|
|
match instances.entry(instance_id) {
|
|
std::collections::btree_map::Entry::Vacant(entry) => {
|
|
entry.insert(instance.clone());
|
|
Ok(instance)
|
|
}
|
|
std::collections::btree_map::Entry::Occupied(_) => Err(
|
|
crate::instance::manager::InstanceCreateError::AlreadyExists { instance_id },
|
|
),
|
|
}
|
|
})
|
|
}
|
|
|
|
fn read_guest_buffer(pointer: u32, length: u32, maximum: usize) -> anyhow::Result<Vec<u8>> {
|
|
let length = usize::try_from(length).expect("u32 fits usize on wasm32");
|
|
if pointer == 0 || length == 0 || length > maximum {
|
|
anyhow::bail!("invalid guest buffer reference");
|
|
}
|
|
with_abi_state(|state| state.read_buffer(pointer, length))
|
|
}
|
|
|
|
#[unsafe(no_mangle)]
|
|
/// Allocates a guest-owned ABI buffer and returns its linear-memory offset.
|
|
///
|
|
/// The runtime may write at most `length` bytes through wasm memory, then
|
|
/// pass the pointer to another lifecycle export and eventually free it.
|
|
pub extern "C" fn easytier_buffer_alloc(length: u32) -> u32 {
|
|
let length = usize::try_from(length).expect("u32 fits usize on wasm32");
|
|
if length == 0 || length > MAX_GUEST_BUFFER_LEN {
|
|
set_abi_error("invalid guest buffer length");
|
|
return 0;
|
|
}
|
|
let mut buffer = vec![0_u8; length].into_boxed_slice();
|
|
let pointer = buffer.as_mut_ptr() as u32;
|
|
if pointer == 0 {
|
|
set_abi_error("guest buffer allocation failed");
|
|
return 0;
|
|
}
|
|
with_abi_state_mut(|state| {
|
|
if state.buffers.contains_key(&pointer) {
|
|
state.set_global_error("guest buffer allocation collided with a live buffer");
|
|
0
|
|
} else {
|
|
state.buffers.insert(pointer, buffer);
|
|
pointer
|
|
}
|
|
})
|
|
}
|
|
|
|
#[unsafe(no_mangle)]
|
|
/// Releases a buffer previously returned by [`easytier_buffer_alloc`].
|
|
pub extern "C" fn easytier_buffer_free(pointer: u32) -> i32 {
|
|
with_abi_state_mut(|state| {
|
|
if state.buffers.remove(&pointer).is_some() {
|
|
0
|
|
} else {
|
|
state.set_global_error(format!("unknown guest buffer: {pointer}"));
|
|
INVALID_INPUT
|
|
}
|
|
})
|
|
}
|
|
|
|
#[unsafe(no_mangle)]
|
|
/// Creates one core instance from a versioned envelope containing TOML.
|
|
///
|
|
/// `config_pointer` must name a live ABI buffer. `packet_sink_handle` and
|
|
/// `event_sink_handle` identify the host sinks used for locally delivered
|
|
/// raw IP packets and best-effort instance events.
|
|
/// Returns zero on failure; retrieve the reason through the error exports.
|
|
pub extern "C" fn easytier_instance_create(
|
|
config_pointer: u32,
|
|
config_length: u32,
|
|
packet_sink_handle: u64,
|
|
event_sink_handle: u64,
|
|
) -> u64 {
|
|
let encoded = match read_guest_buffer(config_pointer, config_length, MAX_CREATE_CONFIG_LEN)
|
|
{
|
|
Ok(encoded) => encoded,
|
|
Err(error) => {
|
|
set_abi_error(error);
|
|
return 0;
|
|
}
|
|
};
|
|
let create_config = match decode_create_config(&encoded) {
|
|
Ok(config) => config,
|
|
Err(error) => {
|
|
set_abi_error(error);
|
|
return 0;
|
|
}
|
|
};
|
|
let config = match create_config.parse_config() {
|
|
Ok(config) => config,
|
|
Err(error) => {
|
|
set_abi_error(error);
|
|
return 0;
|
|
}
|
|
};
|
|
let handle = match with_abi_state_mut(WasiAbiState::allocate_handle) {
|
|
Ok(handle) => handle,
|
|
Err(_) => return 0,
|
|
};
|
|
let instance = manager_create(
|
|
config,
|
|
WasiCreateContext {
|
|
domain: handle,
|
|
environment: create_config.environment,
|
|
packet_sink: HostPacketSinkHandle(packet_sink_handle),
|
|
event_sink: event_sink_handle,
|
|
},
|
|
);
|
|
let instance = match instance {
|
|
Ok(instance) => instance,
|
|
Err(error) => {
|
|
clear_domain(handle);
|
|
set_abi_error(error);
|
|
return 0;
|
|
}
|
|
};
|
|
|
|
with_abi_state_mut(|state| {
|
|
let previous = state.handles.insert(
|
|
handle,
|
|
WasiHandleState {
|
|
instance_id: instance.instance_id(),
|
|
error: String::new(),
|
|
},
|
|
);
|
|
debug_assert!(previous.is_none());
|
|
handle
|
|
})
|
|
}
|
|
|
|
#[unsafe(no_mangle)]
|
|
/// Schedules instance startup. Completion is advanced by subsequent drive calls.
|
|
pub extern "C" fn easytier_instance_start(handle: u64) -> i32 {
|
|
with_instance(handle, |instance| match instance.start() {
|
|
Ok(()) => Ok(0),
|
|
Err(error) => {
|
|
set_instance_error(handle, error);
|
|
Ok(INVALID_STATE)
|
|
}
|
|
})
|
|
}
|
|
|
|
#[unsafe(no_mangle)]
|
|
/// Requests graceful instance shutdown. Completion is advanced by drive calls.
|
|
pub extern "C" fn easytier_instance_stop(handle: u64) -> i32 {
|
|
with_instance(handle, |instance| {
|
|
instance.stop();
|
|
Ok(0)
|
|
})
|
|
}
|
|
|
|
#[unsafe(no_mangle)]
|
|
/// Runs one bounded turn of the instance's externally driven Tokio runtime.
|
|
///
|
|
/// The return value is the current lifecycle state code, or a negative ABI
|
|
/// status on failure. Call after a timer deadline or host completion.
|
|
pub extern "C" fn easytier_instance_drive(handle: u64) -> i32 {
|
|
with_instance(handle, |instance| {
|
|
instance.drive()?;
|
|
Ok(instance.state_code())
|
|
})
|
|
}
|
|
|
|
#[unsafe(no_mangle)]
|
|
/// Wakes tasks whose host I/O operation may have completed.
|
|
///
|
|
/// The runtime calls this after finishing one or more `easytier_host`
|
|
/// operations; it does not itself consume a host completion.
|
|
pub extern "C" fn easytier_instance_notify_completions(handle: u64) -> i32 {
|
|
with_instance(handle, |instance| {
|
|
instance.core.notify_host_completions();
|
|
Ok(0)
|
|
})
|
|
}
|
|
|
|
#[unsafe(no_mangle)]
|
|
/// Returns the current lifecycle state code without running the instance.
|
|
pub extern "C" fn easytier_instance_state(handle: u64) -> i32 {
|
|
with_instance(handle, |instance| Ok(instance.state_code()))
|
|
}
|
|
|
|
/// Returns milliseconds until the next required drive, rounded up. A zero
|
|
/// means lifecycle work remains locally runnable. `i64::MAX` means core is
|
|
/// waiting only for a host completion; negative values are ABI status codes.
|
|
#[unsafe(no_mangle)]
|
|
pub extern "C" fn easytier_instance_next_deadline_millis(handle: u64) -> i64 {
|
|
let instance_id = match with_abi_state_mut(|state| state.begin_instance_call(handle)) {
|
|
Ok(instance_id) => instance_id,
|
|
Err(status) => return i64::from(status),
|
|
};
|
|
let result = match manager_get(instance_id) {
|
|
Some(instance) => instance
|
|
.next_wait_millis()
|
|
.map(|millis| i64::try_from(millis).unwrap_or(i64::MAX))
|
|
.unwrap_or(i64::MAX),
|
|
None => {
|
|
set_instance_error(
|
|
handle,
|
|
format!("core instance {instance_id} is not registered"),
|
|
);
|
|
i64::from(INVALID_STATE)
|
|
}
|
|
};
|
|
with_abi_state_mut(WasiAbiState::finish_instance_call);
|
|
result
|
|
}
|
|
|
|
#[unsafe(no_mangle)]
|
|
/// Copies a raw IP packet from a guest ABI buffer into EasyTier ingress.
|
|
///
|
|
/// Packet processing is asynchronous; success only means the ingress task
|
|
/// was scheduled. The caller retains and may free the source buffer once
|
|
/// this function returns.
|
|
pub extern "C" fn easytier_instance_send_packet(
|
|
handle: u64,
|
|
packet_pointer: u32,
|
|
packet_length: u32,
|
|
) -> i32 {
|
|
with_instance(handle, |instance| {
|
|
let packet = match read_guest_buffer(packet_pointer, packet_length, 1024 * 1024) {
|
|
Ok(packet) => packet,
|
|
Err(error) => {
|
|
set_instance_error(handle, error);
|
|
return Ok(INVALID_INPUT);
|
|
}
|
|
};
|
|
instance.send_packet(packet);
|
|
Ok(0)
|
|
})
|
|
}
|
|
|
|
#[unsafe(no_mangle)]
|
|
/// Destroys an instance and releases its lifecycle, timer, and runtime state.
|
|
pub extern "C" fn easytier_instance_drop(handle: u64) -> i32 {
|
|
let instance_id = match with_abi_state_mut(|state| state.begin_instance_drop(handle)) {
|
|
Ok(instance_id) => instance_id,
|
|
Err(status) => return status,
|
|
};
|
|
let Some(instance) = manager_remove(instance_id) else {
|
|
set_instance_error(
|
|
handle,
|
|
format!("core instance {instance_id} is not registered"),
|
|
);
|
|
with_abi_state_mut(WasiAbiState::finish_instance_call);
|
|
return INVALID_STATE;
|
|
};
|
|
let domain = instance.domain;
|
|
{
|
|
let _domain = enter_domain(domain);
|
|
drop(instance);
|
|
}
|
|
clear_domain(domain);
|
|
with_abi_state_mut(|state| state.finish_instance_drop(handle));
|
|
0
|
|
}
|
|
|
|
#[unsafe(no_mangle)]
|
|
/// Returns the byte length of the most recent lifecycle error for `handle`.
|
|
pub extern "C" fn easytier_instance_error_len(handle: u64) -> u32 {
|
|
with_abi_state(|state| {
|
|
u32::try_from(state.error_for_handle(handle).len()).unwrap_or(u32::MAX)
|
|
})
|
|
}
|
|
|
|
#[unsafe(no_mangle)]
|
|
/// Copies the most recent lifecycle error into a caller-owned ABI buffer.
|
|
///
|
|
/// `capacity` must contain the whole error; on success returns the copied
|
|
/// byte count, otherwise a negative ABI status code.
|
|
pub extern "C" fn easytier_instance_error_copy(
|
|
handle: u64,
|
|
destination: u32,
|
|
capacity: u32,
|
|
) -> i32 {
|
|
with_abi_state_mut(|state| {
|
|
let error = state.error_for_handle(handle).as_bytes().to_vec();
|
|
let capacity = usize::try_from(capacity).expect("u32 fits usize on wasm32");
|
|
let Some(destination) = state.buffers.get_mut(&destination) else {
|
|
return INVALID_INPUT;
|
|
};
|
|
if capacity > destination.len() || capacity < error.len() {
|
|
return INVALID_INPUT;
|
|
}
|
|
destination[..error.len()].copy_from_slice(&error);
|
|
i32::try_from(error.len()).unwrap_or(INVALID_INPUT)
|
|
})
|
|
}
|
|
}
|