From 34e0150c5296eb299d7792317a57047ebefb5d4a Mon Sep 17 00:00:00 2001 From: Charles Eckman Date: Tue, 14 Apr 2026 12:18:19 -0400 Subject: [PATCH 1/6] Quiet a warning about an unused import --- src/component/compute/http_resp.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/component/compute/http_resp.rs b/src/component/compute/http_resp.rs index f6d2c722..a775808b 100644 --- a/src/component/compute/http_resp.rs +++ b/src/component/compute/http_resp.rs @@ -2,7 +2,7 @@ use { crate::{ component::{ bindings::fastly::compute::{http_body, http_resp, http_types, types}, - compute::headers::{get_names, get_values}, + compute::headers::get_names, }, error::Error, linking::{ComponentCtx, SessionView}, @@ -15,6 +15,9 @@ use { wasmtime::component::Resource, }; +#[allow(unused)] // Conditionally unused in the trap test. Quiet the warning. +use crate::component::compute::headers::get_values; + const MAX_HEADER_NAME_LEN: usize = (1 << 16) - 1; impl http_resp::Host for ComponentCtx { From bdc6a72340ba9b4d98debf5726d0045f45516e76 Mon Sep 17 00:00:00 2001 From: Charles Eckman Date: Tue, 14 Apr 2026 09:48:28 -0400 Subject: [PATCH 2/6] Add 'subscribe' hostcalls to resources where they are missing These resources are pollable (`async_io::select`-able) in the WITX ABI, but are not `async-io.pollable` in the WIT ABI. To allow the adapter to shim the differences, add `subscribe` calls to the WIT definitions. --- wasm_abi/wit/deps/fastly/compute.wit | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/wasm_abi/wit/deps/fastly/compute.wit b/wasm_abi/wit/deps/fastly/compute.wit index 78dcaf9c..9b80e134 100644 --- a/wasm_abi/wit/deps/fastly/compute.wit +++ b/wasm_abi/wit/deps/fastly/compute.wit @@ -2182,6 +2182,7 @@ interface cache { use types.{error}; use http-body.{body}; use http-req.{request}; + use async-io.{pollable}; /// The outcome of a cache lookup (either bare or as part of a cache transaction) resource entry { @@ -2294,6 +2295,10 @@ interface cache { /// Useful if there is an error before streaming is possible, for example if a backend is /// unreachable. transaction-cancel: func() -> result<_, error>; + + /// Returns a `pollable` that can be used to wait for this `entry` to be + /// ready. + subscribe: func() -> pollable; } /// Handle that can be used to check whether or not a cache lookup is waiting on another client. use async-io.{pollable as pending-entry}; @@ -2352,6 +2357,10 @@ interface cache { get-user-metadata: func( max-len: u64, ) -> result>, error>; + + /// Returns a `pollable` that can be used to wait for this `replace-entry` + /// to be ready. + subscribe: func() -> pollable; } type object-length = u64; @@ -2561,6 +2570,7 @@ interface http-cache { use http-resp.{response, response-with-body}; use backend.{backend}; use cache.{lookup-state, object-length, duration-ns, cache-hit-count}; + use async-io.{pollable}; /// An HTTP Cache transaction. resource entry { @@ -2759,6 +2769,10 @@ interface http-cache { /// requests. Consider using `transaction-record-not-cacheable` to make lookups for this request /// bypass the cache. transaction-abandon: func() -> result<_, error>; + + /// Returns a `pollable` that can be used to wait for this `entry` to be + /// ready. + subscribe: func() -> pollable; } /// The suggested action to take for spec-recommended behavior following From 08e3938fcc6f2f4b642158341a47851bce4c69c0 Mon Sep 17 00:00:00 2001 From: Charles Eckman Date: Tue, 14 Apr 2026 12:16:41 -0400 Subject: [PATCH 3/6] Add subscribe methods to incompatible pollables --- src/component.rs | 1 + src/component/compute/cache.rs | 16 ++++++++++++++-- src/component/compute/http_cache.rs | 12 +++++++++++- src/session.rs | 16 +++++++++++++++- 4 files changed, 41 insertions(+), 4 deletions(-) diff --git a/src/component.rs b/src/component.rs index f330a627..529b010c 100644 --- a/src/component.rs +++ b/src/component.rs @@ -61,6 +61,7 @@ pub(crate) mod bindings { "fastly:compute/kv-store.[method]store.list-async": async | tracing, "fastly:compute/kv-store.[method]store.lookup": async | tracing, "fastly:compute/kv-store.[method]store.lookup-async": async | tracing, + "fastly:compute/http-cache.[method]entry.subscribe": tracing | trappable, "fastly:compute/http-downstream.next-request": async | tracing, "fastly:compute/http-body.read": async | tracing, "fastly:compute/backend.register-dynamic-backend": async | tracing, diff --git a/src/component/compute/cache.rs b/src/component/compute/cache.rs index e50263e0..bd56e381 100644 --- a/src/component/compute/cache.rs +++ b/src/component/compute/cache.rs @@ -1,12 +1,12 @@ use { - crate::component::bindings::fastly::compute::{cache as api, http_body, types}, crate::{ body::Body, cache::{self, CacheKey, SurrogateKeySet, VaryRule, WriteOptions}, + component::bindings::fastly::compute::{cache as api, http_body, types}, error::Error, linking::{ComponentCtx, SessionView}, session::{PeekableTask, PendingCacheTask, Session}, - wiggle_abi::types::{CacheBusyHandle, CacheHandle}, + wiggle_abi::types::{CacheBusyHandle, CacheHandle, CacheReplaceHandle}, }, bytes::Bytes, http::HeaderMap, @@ -294,6 +294,12 @@ impl api::HostReplaceEntry for ComponentCtx { .into()) } + fn subscribe(&mut self, handle: Resource) -> Resource { + let host_handle = crate::session::AsyncItemHandle::from(CacheReplaceHandle::from(handle)); + let guest_handle = crate::wiggle_abi::types::AsyncItemHandle::from(host_handle); + guest_handle.into() + } + fn drop(&mut self, _entry: Resource) -> wasmtime::Result<()> { Ok(()) } @@ -594,6 +600,12 @@ impl api::HostEntry for ComponentCtx { .into()) } + fn subscribe(&mut self, handle: Resource) -> Resource { + let host_handle = crate::session::AsyncItemHandle::from(CacheHandle::from(handle)); + let guest_handle = crate::wiggle_abi::types::AsyncItemHandle::from(host_handle); + guest_handle.into() + } + fn drop(&mut self, _entry: Resource) -> wasmtime::Result<()> { Ok(()) } diff --git a/src/component/compute/http_cache.rs b/src/component/compute/http_cache.rs index 88a957f0..4bafe31c 100644 --- a/src/component/compute/http_cache.rs +++ b/src/component/compute/http_cache.rs @@ -1,5 +1,5 @@ use { - crate::component::bindings::fastly::compute::{http_body, http_cache, types}, + crate::component::bindings::fastly::compute::{async_io, http_body, http_cache, types}, crate::{error::Error, linking::ComponentCtx}, wasmtime::component::Resource, }; @@ -350,6 +350,16 @@ impl http_cache::HostEntry for ComponentCtx { .into()) } + fn subscribe( + &mut self, + _handle: Resource, + ) -> Result, anyhow::Error> { + Err(Error::Unsupported { + msg: "HTTP Cache API primitives not yet supported", + } + .into()) + } + fn drop(&mut self, _handle: Resource) -> wasmtime::Result<()> { Err(Error::Unsupported { msg: "HTTP Cache API primitives not yet supported", diff --git a/src/session.rs b/src/session.rs index 0f724f0f..9bd36e29 100644 --- a/src/session.rs +++ b/src/session.rs @@ -20,7 +20,9 @@ use std::time::Duration; use crate::cache::{Cache, CacheEntry}; use crate::linking::Limiter; use crate::object_store::KvStoreError; -use crate::wiggle_abi::types::{CacheBusyHandle, CacheHandle, FramingHeadersMode}; +use crate::wiggle_abi::types::{ + CacheBusyHandle, CacheHandle, CacheReplaceHandle, FramingHeadersMode, +}; use { self::downstream::DownstreamResponseState, @@ -1577,6 +1579,18 @@ impl From for AsyncItemHandle { } } +impl From for CacheReplaceHandle { + fn from(h: AsyncItemHandle) -> CacheReplaceHandle { + CacheReplaceHandle::from(h.as_u32()) + } +} + +impl From for AsyncItemHandle { + fn from(h: CacheReplaceHandle) -> AsyncItemHandle { + AsyncItemHandle::from_u32(h.into()) + } +} + impl From for CacheBusyHandle { fn from(h: AsyncItemHandle) -> CacheBusyHandle { CacheBusyHandle::from(h.as_u32()) From be0bf13384d499aa2d9312f640b1b7afc9c37eb5 Mon Sep 17 00:00:00 2001 From: Charles Eckman Date: Tue, 14 Apr 2026 12:29:15 -0400 Subject: [PATCH 4/6] Avoid dropping Pollable handles created by subscribe() --- src/component/compute/async_io.rs | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/src/component/compute/async_io.rs b/src/component/compute/async_io.rs index 4a9b6fe9..0cd768a6 100644 --- a/src/component/compute/async_io.rs +++ b/src/component/compute/async_io.rs @@ -1,7 +1,8 @@ use { - crate::component::bindings::fastly::compute::async_io, crate::{ + component::bindings::fastly::compute::async_io, linking::{ComponentCtx, SessionView}, + session::AsyncItem, wiggle_abi, }, anyhow::bail, @@ -56,13 +57,33 @@ impl async_io::HostPollable for ComponentCtx { .is_some() } - fn drop(&mut self, handle: Resource) -> wasmtime::Result<()> { - let handle = wiggle_abi::types::AsyncItemHandle::from(handle).into(); + fn drop(&mut self, h: Resource) -> wasmtime::Result<()> { + let handle: wiggle_abi::types::AsyncItemHandle = h.into(); + + { + let it = self.session_mut().async_item_mut(handle.into())?; + + // In the WIT ABI, CacheEntry, CacheReplace, and HttpCacheEntry AsyncItems are not + // async_io::Pollables. Insteady, their primary handles "own" the AsyncItem, + // and the Pollable "borrows" from it. + // + // But! Those handles have the same ID when presented to the host. + // So if we encounter a Pollable to one of those types here, we need to keep + // the AsyncItem in the table, and just let drop() clean up the Resource. + // + // Note that we don't cover HTTP cache items here; we don't support the HTTP caching + // API, so the guest won't have a valid handle to an HTTP cache item. + if matches!(*it, AsyncItem::PendingCache(_)) { + // Don't remove from the session.async_items set; this is "just" the Pollable for the + // item, not the real thing. + return Ok(()); + }; + } // Use `.take_async_item` instead of manipulating // `self.session_mut().async_items` directly, so that any extra state // associated with the item is also cleared. - let _ = self.session_mut().take_async_item(handle).unwrap(); + let _ = self.session_mut().take_async_item(handle.into())?; Ok(()) } From f1358b3b780fbc4b17902b927a2b21018fe910b8 Mon Sep 17 00:00:00 2001 From: Charles Eckman Date: Tue, 14 Apr 2026 12:59:25 -0400 Subject: [PATCH 5/6] Shim async-io.pollable in the adapter --- wasm_abi/adapter/src/fastly/cache.rs | 107 +++++++++--------- wasm_abi/adapter/src/fastly/core.rs | 107 ++++++++++++++++-- wasm_abi/adapter/src/fastly/dynamic_types.rs | 113 +++++++++++++++++++ wasm_abi/adapter/src/fastly/mod.rs | 1 + 4 files changed, 266 insertions(+), 62 deletions(-) create mode 100644 wasm_abi/adapter/src/fastly/dynamic_types.rs diff --git a/wasm_abi/adapter/src/fastly/cache.rs b/wasm_abi/adapter/src/fastly/cache.rs index b61e0893..0a1c5888 100644 --- a/wasm_abi/adapter/src/fastly/cache.rs +++ b/wasm_abi/adapter/src/fastly/cache.rs @@ -125,7 +125,10 @@ bitflags::bitflags! { mod cache { use super::*; - use crate::bindings::fastly::compute::{cache, http_req}; + use crate::{ + bindings::fastly::compute::{cache, http_req}, + fastly::dynamic_types::{self, DynamicType}, + }; use core::slice; impl From for CacheLookupState { @@ -219,8 +222,9 @@ mod cache { match res { Ok(res) => { + let handle = dynamic_types::set_type(res.take_handle(), DynamicType::CacheEntry); unsafe { - *main_ptr!(cache_handle_out) = res.take_handle(); + *main_ptr!(cache_handle_out) = handle; } FastlyStatus::OK } @@ -386,8 +390,9 @@ mod cache { match res { Ok(res) => { + let handle = dynamic_types::set_type(res.take_handle(), DynamicType::CacheEntry); unsafe { - *main_ptr!(cache_handle_out) = res.take_handle(); + *main_ptr!(cache_handle_out) = handle; } FastlyStatus::OK } @@ -433,6 +438,8 @@ mod cache { match res { Ok(res) => { + // In the WIT ABI, a CacheBusy handle _is_ a Pollable, so doesn't need the + // dynamic_type shim. unsafe { *main_ptr!(cache_handle_out) = res.take_handle(); } @@ -460,8 +467,9 @@ mod cache { let cache_busy_handle = unsafe { cache::PendingEntry::from_handle(handle) }; match cache::await_entry(cache_busy_handle) { Ok(res) => { + let handle = dynamic_types::set_type(res.take_handle(), DynamicType::CacheEntry); unsafe { - *main_ptr!(cache_handle_out) = res.take_handle(); + *main_ptr!(cache_handle_out) = handle; } // Remember that we just consumed `handle` so that if there's @@ -484,6 +492,7 @@ mod cache { options: *const CacheWriteOptions, body_handle_out: *mut BodyHandle, ) -> FastlyStatus { + let handle = dynamic_types::raw_handle(handle, DynamicType::CacheEntry); if options_mask.contains(CacheWriteOptionsMask::SERVICE) { return FastlyStatus::UNSUPPORTED; } @@ -530,6 +539,7 @@ mod cache { body_handle_out: *mut BodyHandle, cache_handle_out: *mut CacheHandle, ) -> FastlyStatus { + let handle = dynamic_types::raw_handle(handle, DynamicType::CacheEntry); if options_mask.contains(CacheWriteOptionsMask::SERVICE) { return FastlyStatus::UNSUPPORTED; } @@ -559,9 +569,12 @@ mod cache { match res { Ok((body_handle, cache_handle)) => { + let handle = + dynamic_types::set_type(cache_handle.take_handle(), DynamicType::CacheEntry); + unsafe { *main_ptr!(body_handle_out) = body_handle.take_handle(); - *main_ptr!(cache_handle_out) = cache_handle.take_handle(); + *main_ptr!(cache_handle_out) = handle; } FastlyStatus::OK } @@ -575,6 +588,7 @@ mod cache { options_mask: CacheWriteOptionsMask, options: *const CacheWriteOptions, ) -> FastlyStatus { + let handle = dynamic_types::raw_handle(handle, DynamicType::CacheEntry); if options_mask.contains(CacheWriteOptionsMask::SERVICE) { return FastlyStatus::UNSUPPORTED; } @@ -607,6 +621,7 @@ mod cache { #[export_name = "fastly_cache#transaction_cancel"] pub fn transaction_cancel(handle: CacheHandle) -> FastlyStatus { + let handle = dynamic_types::raw_handle(handle, DynamicType::CacheEntry); let handle = ManuallyDrop::new(unsafe { cache::Entry::from_handle(handle) }); convert_result(handle.transaction_cancel()) } @@ -652,14 +667,22 @@ mod cache { } // The witx `close` is shared between cache entries and replace entries. - // We set a bit in the returned handle index to distinguish the two. - if is_replace_entry(handle) { - let handle = decode_replace_entry(handle); - let handle = unsafe { cache::ReplaceEntry::from_handle(handle) }; - convert_result(cache::close_replace_entry(handle)) - } else { - let handle = unsafe { cache::Entry::from_handle(handle) }; - convert_result(cache::close_entry(handle)) + // We use a mask in the returned handle index to distinguish the two. + let (ty, raw) = dynamic_types::parts(handle); + match ty { + DynamicType::CacheReplaceEntry => { + let handle = unsafe { cache::ReplaceEntry::from_handle(raw) }; + convert_result(cache::close_replace_entry(handle)) + } + DynamicType::CacheEntry => { + let handle = unsafe { cache::Entry::from_handle(raw) }; + convert_result(cache::close_entry(handle)) + } + _ => { + // Not a valid handle for this call. We can't panic, but we can return "bad + // handle": + FastlyStatus::BADF + } } } @@ -668,6 +691,7 @@ mod cache { handle: CacheHandle, cache_lookup_state_out: *mut CacheLookupState, ) -> FastlyStatus { + let handle = dynamic_types::raw_handle(handle, DynamicType::CacheEntry); let handle = ManuallyDrop::new(unsafe { cache::Entry::from_handle(handle) }); match handle.get_state() { Ok(res) => { @@ -687,6 +711,7 @@ mod cache { user_metadata_out_len: usize, nwritten_out: *mut usize, ) -> FastlyStatus { + let handle = dynamic_types::raw_handle(handle, DynamicType::CacheEntry); let handle = ManuallyDrop::new(unsafe { cache::Entry::from_handle(handle) }); alloc_result_opt!( unsafe_main_ptr!(user_metadata_out_ptr), @@ -717,6 +742,7 @@ mod cache { options: *const CacheGetBodyOptions, body_handle_out: *mut BodyHandle, ) -> FastlyStatus { + let handle = dynamic_types::raw_handle(handle, DynamicType::CacheEntry); let handle = ManuallyDrop::new(unsafe { cache::Entry::from_handle(handle) }); let options = unsafe { cache::GetBodyOptions::from((options_mask, *main_ptr!(options))) }; @@ -737,6 +763,7 @@ mod cache { #[export_name = "fastly_cache#get_length"] pub fn get_length(handle: CacheHandle, length_out: *mut CacheObjectLength) -> FastlyStatus { + let handle = dynamic_types::raw_handle(handle, DynamicType::CacheEntry); let handle = ManuallyDrop::new(unsafe { cache::Entry::from_handle(handle) }); match handle.get_length() { Ok(Some(res)) => { @@ -752,6 +779,7 @@ mod cache { #[export_name = "fastly_cache#get_max_age_ns"] pub fn get_max_age_ns(handle: CacheHandle, duration_out: *mut CacheDurationNs) -> FastlyStatus { + let handle = dynamic_types::raw_handle(handle, DynamicType::CacheEntry); let handle = ManuallyDrop::new(unsafe { cache::Entry::from_handle(handle) }); match handle.get_max_age_ns() { Ok(Some(res)) => { @@ -770,6 +798,7 @@ mod cache { handle: CacheHandle, duration_out: *mut CacheDurationNs, ) -> FastlyStatus { + let handle = dynamic_types::raw_handle(handle, DynamicType::CacheEntry); let handle = ManuallyDrop::new(unsafe { cache::Entry::from_handle(handle) }); match handle.get_stale_while_revalidate_ns() { Ok(Some(res)) => { @@ -785,6 +814,7 @@ mod cache { #[export_name = "fastly_cache#get_age_ns"] pub fn get_age_ns(handle: CacheHandle, duration_out: *mut CacheDurationNs) -> FastlyStatus { + let handle = dynamic_types::raw_handle(handle, DynamicType::CacheEntry); let handle = ManuallyDrop::new(unsafe { cache::Entry::from_handle(handle) }); match handle.get_age_ns() { Ok(Some(res)) => { @@ -800,6 +830,7 @@ mod cache { #[export_name = "fastly_cache#get_hits"] pub fn get_hits(handle: CacheHandle, hits_out: *mut CacheHitCount) -> FastlyStatus { + let handle = dynamic_types::raw_handle(handle, DynamicType::CacheEntry); let handle = ManuallyDrop::new(unsafe { cache::Entry::from_handle(handle) }); match handle.get_hits() { Ok(Some(res)) => { @@ -862,7 +893,8 @@ mod cache { match res { Ok(res) => { unsafe { - *main_ptr!(cache_handle_out) = encode_replace_entry(res.take_handle()); + *main_ptr!(cache_handle_out) = + dynamic_types::set_type(res.take_handle(), DynamicType::CacheReplaceEntry); } // We just created a new `CacheReplaceHandle` so forget the @@ -893,7 +925,7 @@ mod cache { let options = unsafe_main_ptr!(options); - let replace_handle = decode_replace_entry(handle); + let replace_handle = dynamic_types::raw_handle(handle, DynamicType::CacheReplaceEntry); let replace_handle = unsafe { cache::ReplaceEntry::from_handle(replace_handle) }; let request_headers = if options_mask.contains(CacheWriteOptionsMask::REQUEST_HEADERS) { match unsafe { (*options).request_headers } { @@ -940,7 +972,7 @@ mod cache { handle: CacheReplaceHandle, duration_out: *mut CacheDurationNs, ) -> FastlyStatus { - let handle = decode_replace_entry(handle); + let handle = dynamic_types::raw_handle(handle, DynamicType::CacheReplaceEntry); let handle = ManuallyDrop::new(unsafe { cache::ReplaceEntry::from_handle(handle) }); match handle.get_age_ns() { Ok(Some(res)) => { @@ -962,7 +994,7 @@ mod cache { options: *const CacheGetBodyOptions, body_handle_out: *mut BodyHandle, ) -> FastlyStatus { - let handle = decode_replace_entry(handle); + let handle = dynamic_types::raw_handle(handle, DynamicType::CacheReplaceEntry); let handle = ManuallyDrop::new(unsafe { cache::ReplaceEntry::from_handle(handle) }); let options = unsafe { cache::GetBodyOptions::from((options_mask, *main_ptr!(options))) }; @@ -988,7 +1020,7 @@ mod cache { handle: CacheReplaceHandle, hits_out: *mut CacheHitCount, ) -> FastlyStatus { - let handle = decode_replace_entry(handle); + let handle = dynamic_types::raw_handle(handle, DynamicType::CacheReplaceEntry); let handle = ManuallyDrop::new(unsafe { cache::ReplaceEntry::from_handle(handle) }); match handle.get_hits() { Ok(Some(res)) => { @@ -1008,7 +1040,7 @@ mod cache { handle: CacheReplaceHandle, length_out: *mut CacheObjectLength, ) -> FastlyStatus { - let handle = decode_replace_entry(handle); + let handle = dynamic_types::raw_handle(handle, DynamicType::CacheReplaceEntry); let handle = ManuallyDrop::new(unsafe { cache::ReplaceEntry::from_handle(handle) }); match handle.get_length() { Ok(Some(res)) => { @@ -1028,7 +1060,7 @@ mod cache { handle: CacheReplaceHandle, duration_out: *mut CacheDurationNs, ) -> FastlyStatus { - let handle = decode_replace_entry(handle); + let handle = dynamic_types::raw_handle(handle, DynamicType::CacheReplaceEntry); let handle = ManuallyDrop::new(unsafe { cache::ReplaceEntry::from_handle(handle) }); match handle.get_max_age_ns() { Ok(Some(res)) => { @@ -1048,7 +1080,7 @@ mod cache { handle: CacheReplaceHandle, duration_out: *mut CacheDurationNs, ) -> FastlyStatus { - let handle = decode_replace_entry(handle); + let handle = dynamic_types::raw_handle(handle, DynamicType::CacheReplaceEntry); let handle = ManuallyDrop::new(unsafe { cache::ReplaceEntry::from_handle(handle) }); match handle.get_stale_while_revalidate_ns() { Ok(Some(res)) => { @@ -1068,7 +1100,7 @@ mod cache { handle: CacheReplaceHandle, cache_lookup_state_out: *mut CacheLookupState, ) -> FastlyStatus { - let handle = decode_replace_entry(handle); + let handle = dynamic_types::raw_handle(handle, DynamicType::CacheReplaceEntry); let handle = ManuallyDrop::new(unsafe { cache::ReplaceEntry::from_handle(handle) }); match handle.get_state() { Ok(Some(res)) => { @@ -1090,7 +1122,7 @@ mod cache { user_metadata_out_len: usize, nwritten_out: *mut usize, ) -> FastlyStatus { - let handle = decode_replace_entry(handle); + let handle = dynamic_types::raw_handle(handle, DynamicType::CacheReplaceEntry); let handle = ManuallyDrop::new(unsafe { cache::ReplaceEntry::from_handle(handle) }); alloc_result_opt!( unsafe_main_ptr!(user_metadata_out_ptr), @@ -1100,32 +1132,3 @@ mod cache { ) } } - -/// The witx `fastly_cache#close` function works on both `CacheHandle` values and -/// `CacheReplace` values. In the WIT API, these are separate resources. To -/// distinguish them, we add a bit to the handle value that we expose to witx that -/// otherwise not used in the Canonical ABI. -/// -/// The Canonical ABI doesn't use the high four bits, but we don't use the -/// most-significant bit here to avoid handle values that may look negative to -/// witx users. -const REPLACE_ENTRY_MARKER: u32 = 0x4000_0000; - -/// Convert a `CacheReplaceHandle` index into a `CacheHandle` index with the -/// special flag set indicating that it's a replace entry. -fn encode_replace_entry(cache_entry: CacheReplaceHandle) -> CacheHandle { - assert!(!is_replace_entry(cache_entry)); - cache_entry | REPLACE_ENTRY_MARKER -} - -/// Convert a `CacheHandle` index that holds an encoded replace entry into a -/// `CacheReplaceHandle` index. -fn decode_replace_entry(cache_entry: CacheHandle) -> CacheReplaceHandle { - assert!(is_replace_entry(cache_entry)); - cache_entry & !REPLACE_ENTRY_MARKER -} - -/// Test whether the given `CacheHandle` holds an encoded replace entry. -fn is_replace_entry(cache_entry: CacheHandle) -> bool { - (cache_entry & REPLACE_ENTRY_MARKER) == REPLACE_ENTRY_MARKER -} diff --git a/wasm_abi/adapter/src/fastly/core.rs b/wasm_abi/adapter/src/fastly/core.rs index 383af0b7..eef0bb78 100644 --- a/wasm_abi/adapter/src/fastly/core.rs +++ b/wasm_abi/adapter/src/fastly/core.rs @@ -4695,7 +4695,10 @@ pub mod fastly_acl { pub mod fastly_async_io { use super::*; - use crate::bindings::fastly::compute::async_io; + use crate::{ + bindings::fastly::compute::async_io, + fastly::dynamic_types::{self, DynamicType}, + }; use core::slice; #[export_name = "fastly_async_io#select"] @@ -4708,12 +4711,15 @@ pub mod fastly_async_io { unsafe { let refs = slice::from_raw_parts(main_ptr!(async_item_handles), async_item_handles_len); - // In the witx ABI, a `timeout_ms` value of 0 means no timeout. - *main_ptr!(done_index_out) = if timeout_ms == 0 { - select_wrapper(refs) - } else { - select_with_timeout_wrapper(refs, timeout_ms).unwrap_or(u32::MAX) - }; + // If we have any handles that require dynamic typing, handle them + // specially. + for handle in refs { + if !dynamic_types::is_other(*handle) { + return select_with_dynamic_types(refs, timeout_ms, done_index_out); + } + } + + *main_ptr!(done_index_out) = select_with_maybe_timeout_wrapper(refs, timeout_ms); FastlyStatus::OK } @@ -4722,12 +4728,93 @@ pub mod fastly_async_io { #[export_name = "fastly_async_io#is_ready"] pub fn is_ready(async_item_handle: AsyncItemHandle, ready_out: *mut u32) -> FastlyStatus { unsafe { - let async_item_handle = - ManuallyDrop::new(async_io::Pollable::from_handle(async_item_handle)); - *main_ptr!(ready_out) = async_item_handle.is_ready().into(); + let is_ready: bool = if dynamic_types::is_other(async_item_handle) { + let async_item_handle = + ManuallyDrop::new(async_io::Pollable::from_handle(async_item_handle)); + async_item_handle.is_ready() + } else { + let pollable = get_pollable(async_item_handle); + let async_item_handle = + ManuallyDrop::new(async_io::Pollable::from_handle(pollable)); + let is_ready = async_item_handle.is_ready(); + drop_pollable(async_item_handle.take_handle(), pollable); + is_ready + }; + + *main_ptr!(ready_out) = is_ready.into(); FastlyStatus::OK } } + + #[cold] + fn select_with_dynamic_types( + refs: &[u32], + timeout_ms: u32, + done_index_out: *mut u32, + ) -> FastlyStatus { + crate::State::with::(|state| { + unsafe { + // Allocate a new handle array. + let mut alloc = state.temporary_alloc(); + let buf = alloc.alloc( + core::mem::align_of::(), + core::mem::size_of::() * refs.len(), + ); + let buf = core::slice::from_raw_parts_mut(buf.cast::(), refs.len()); + + // For each handle with a dynamic type that isn't a `pollable`, + // call `.subscribe()` to obtain a `pollable`. + for i in 0..refs.len() { + let new = get_pollable(refs[i]); + buf[i] = new; + } + + *main_ptr!(done_index_out) = select_with_maybe_timeout_wrapper(buf, timeout_ms); + + // Free the `pollable`s we created. + for i in 0..refs.len() { + drop_pollable(refs[i], buf[i]); + } + + Ok(()) + } + }) + } + + fn select_with_maybe_timeout_wrapper(hs: &[u32], timeout_ms: u32) -> u32 { + // In the witx ABI, a `timeout_ms` value of 0 means no timeout. + if timeout_ms == 0 { + select_wrapper(hs) + } else { + select_with_timeout_wrapper(hs, timeout_ms).unwrap_or(u32::MAX) + } + } + + unsafe fn get_pollable(handle: AsyncItemHandle) -> AsyncItemHandle { + use crate::bindings::fastly::compute::{cache, http_cache}; + + match dynamic_types::parts(handle) { + (DynamicType::Other, _) => handle, + (DynamicType::CacheEntry, raw) => { + let cache_entry = ManuallyDrop::new(cache::Entry::from_handle(raw)); + cache_entry.subscribe().take_handle() + } + (DynamicType::CacheReplaceEntry, raw) => { + let cache_replace_entry = ManuallyDrop::new(cache::ReplaceEntry::from_handle(raw)); + cache_replace_entry.subscribe().take_handle() + } + (DynamicType::HttpCacheEntry, raw) => { + let http_cache_entry = ManuallyDrop::new(http_cache::Entry::from_handle(raw)); + http_cache_entry.subscribe().take_handle() + } + } + } + + unsafe fn drop_pollable(handle: AsyncItemHandle, pollable: AsyncItemHandle) { + if !dynamic_types::is_other(handle) { + drop(async_io::Pollable::from_handle(pollable)); + } + } } pub mod fastly_purge { diff --git a/wasm_abi/adapter/src/fastly/dynamic_types.rs b/wasm_abi/adapter/src/fastly/dynamic_types.rs new file mode 100644 index 00000000..35ada58d --- /dev/null +++ b/wasm_abi/adapter/src/fastly/dynamic_types.rs @@ -0,0 +1,113 @@ +//! Limited dynamic typing for handle values. +//! +//! In order to emulate the behavior of the original WITX hostcalls, where +//! handles were just a plain `u32` and some hostcalls could work with +//! multiple types of resources, we use the unused high bits of the `u32` +//! values to encode a dynamic type ID in the adapter, so that we can detect +//! handles of different types and emulate the needed behavior. + +use crate::fastly::AsyncItemHandle; + +/// Reserve bits used to encode extra information in handle "index" values. +/// +/// The Rust SDK uses u32::MAX or, in one case, u32::MAX - 1, as an "invalid handle" signal. +/// We treat all "negative" values as invalid values, preserving without modification. +/// +/// The Canonical ABI doesn't use the high four bits. +/// We mask all four high bits, but use only bits {30, 29, 28} in our values, so we consider all +/// negative values "invalid". +const DYNAMIC_TYPE_MASK: u32 = 0xF000_0000; +const INVALID_HANDLE_MASK: u32 = 0x8000_0000; + +/// All resources that don't need special handling. +const OTHER_TYPE: u32 = 0x0000_0000; +/// `cache.entry` +const CACHE_ENTRY_TYPE: u32 = 0x1000_0000; +/// `cache.replace-entry` +const CACHE_REPLACE_ENTRY_TYPE: u32 = 0x2000_0000; +/// `http-cache.entry` +const HTTP_CACHE_ENTRY_TYPE: u32 = 0x3000_0000; + +/// An `enum` of the different types we need dynamic typing for. +#[derive(Copy, Clone)] +pub enum DynamicType { + Other, + CacheEntry, + CacheReplaceEntry, + HttpCacheEntry, +} + +/// Returns true if this is an "invalid"-flagged handle. +#[inline] +pub const fn is_invalid(handle: AsyncItemHandle) -> bool { + (handle & INVALID_HANDLE_MASK) == INVALID_HANDLE_MASK +} + +/// Test whether the dynamic type encoding in `handle`'s bits is `ty`. +pub fn is_type(handle: AsyncItemHandle, ty: DynamicType) -> bool { + // We consider invalid handles as inhabitants of every type. + is_invalid(handle) + || matches!( + ((handle & DYNAMIC_TYPE_MASK), ty), + (OTHER_TYPE, DynamicType::Other) + | (CACHE_ENTRY_TYPE, DynamicType::CacheEntry) + | (CACHE_REPLACE_ENTRY_TYPE, DynamicType::CacheReplaceEntry) + | (HTTP_CACHE_ENTRY_TYPE, DynamicType::HttpCacheEntry) + ) +} + +/// Test whether the type of `handle` is `DynamicType::Other`. +pub fn is_other(handle: AsyncItemHandle) -> bool { + is_type(handle, DynamicType::Other) +} + +/// Return the dynamic type and the raw handle value. +pub fn parts(handle: AsyncItemHandle) -> (DynamicType, AsyncItemHandle) { + let ty = match handle & DYNAMIC_TYPE_MASK { + OTHER_TYPE => DynamicType::Other, + CACHE_ENTRY_TYPE => DynamicType::CacheEntry, + CACHE_REPLACE_ENTRY_TYPE => DynamicType::CacheReplaceEntry, + HTTP_CACHE_ENTRY_TYPE => DynamicType::HttpCacheEntry, + // Categorize all invalid handles as "other": we don't know how they came about. + _ if is_invalid(handle) => DynamicType::Other, + // All negative handles are invalid, so we've covered all cases that we actually produce. + _ => unreachable!("invalid handle for parts()"), + }; + let raw = if is_invalid(handle) { + handle + } else { + raw_handle_unchecked(handle) + }; + (ty, raw) +} + +/// Return the raw handle value. +pub fn raw_handle(handle: AsyncItemHandle, ty: DynamicType) -> AsyncItemHandle { + if is_invalid(handle) { + handle + } else { + assert!(is_type(handle, ty)); + raw_handle_unchecked(handle) + } +} + +/// Return the raw handle value, without asserting the type. +fn raw_handle_unchecked(handle: AsyncItemHandle) -> AsyncItemHandle { + handle & !DYNAMIC_TYPE_MASK +} + +/// Return a handle value with the dynamic type bits set. +pub fn set_type(handle: AsyncItemHandle, ty: DynamicType) -> AsyncItemHandle { + if is_invalid(handle) { + // Preserve invalid handles verbatim. + return handle; + } + assert!(is_type(handle, DynamicType::Other)); + handle + | match ty { + DynamicType::Other => OTHER_TYPE, + DynamicType::CacheEntry => CACHE_ENTRY_TYPE, + DynamicType::CacheReplaceEntry => CACHE_REPLACE_ENTRY_TYPE, + DynamicType::HttpCacheEntry => HTTP_CACHE_ENTRY_TYPE, + } +} diff --git a/wasm_abi/adapter/src/fastly/mod.rs b/wasm_abi/adapter/src/fastly/mod.rs index c258eab7..f30354b6 100644 --- a/wasm_abi/adapter/src/fastly/mod.rs +++ b/wasm_abi/adapter/src/fastly/mod.rs @@ -1,6 +1,7 @@ mod cache; mod config_store; mod core; +mod dynamic_types; mod error; mod http_cache; mod macros; From 7836cd261c78227565a08f00790197cc6d0d52d3 Mon Sep 17 00:00:00 2001 From: Charles Eckman Date: Tue, 14 Apr 2026 13:17:55 -0400 Subject: [PATCH 6/6] Add changelog entry --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d3168d35..750e1d0c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ## Unreleased +- Add adapter shim for `async-io.select`-incompatible handles. ([#600](https://github.com/fastly/Viceroy/pull/600)) - Add stub implementations for resvpnproxy hostcalls. ## 0.16.5 (2026-03-23)