Skip to main content

actr_framework/guest/
mod.rs

1//! Guest-side runtime module.
2//!
3//! Provides the unified [`entry!`] macro and platform-specific runtime
4//! glue. Actor developers write one `entry!(MyActor)`; the macro selects
5//! the correct ABI at compile time based on the target.
6//!
7//! # Execution contract
8//!
9//! - One loaded guest instance corresponds to one logical actor instance.
10//! - The runtime serialises dispatch into the guest instance. Concurrent
11//!   dispatches within the same instance are forbidden by the host
12//!   (wasmtime enforces this via `&mut Store<HostState>`; dynclib hosts
13//!   enforce via handle ownership).
14//!
15//! # Supported platforms
16//!
17//! - **WASM Component Model** (`target_arch = "wasm32"`, no `web` feature):
18//!   wit-bindgen generates the `Guest` trait + `host` imports from
19//!   `core/framework/wit/actr-workload.wit`; the [`entry!`] macro
20//!   produces an adapter that bridges the user's [`Workload`] impl into
21//!   the generated `Guest`. Targets `wasm32-wasip2` and requires
22//!   `wasm-component-ld 0.5.22+` as the linker (see
23//!   `experiments/component-spike-async/REPORT.md`).
24//! - **Web ABI / wasm-bindgen** (`target_arch = "wasm32"` + `feature = "web"`):
25//!   expands to a [`wasm_bindgen(start)`][wbgstart] bootstrap that wraps the
26//!   user [`Workload`] in `web::WebWorkloadAdapter` and hands it to
27//!   `actr_web_abi::host::register_workload`. Per Option U γ-unified §4.5
28//!   the same user source compiles against both wasm32 ABIs; only the
29//!   macro expansion differs. Targets `wasm32-unknown-unknown`.
30//! - **cdylib** (`feature = "cdylib"`): HostVTable function-pointer
31//!   bridge used for native shared-library guests (iOS / Android).
32//!
33//! [wbgstart]: https://rustwasm.github.io/wasm-bindgen/reference/attributes/on-rust-exports/start.html
34
35pub mod dynclib_abi;
36pub mod vtable;
37
38/// Decode a dynclib `WebRtcPeerStatus` discriminant (see
39/// [`dynclib_abi::webrtc_peer_status`]) into the framework enum. Used by the
40/// `entry!` macro when materialising [`crate::PeerEvent`] from `PeerEventV1`.
41pub fn peer_status_from_v1(status: u32) -> Option<crate::WebRtcPeerStatus> {
42    use crate::guest::dynclib_abi::webrtc_peer_status as st;
43    match status {
44        st::IDLE => Some(crate::WebRtcPeerStatus::Idle),
45        st::CONNECTING => Some(crate::WebRtcPeerStatus::Connecting),
46        st::CONNECTED => Some(crate::WebRtcPeerStatus::Connected),
47        st::RECOVERING => Some(crate::WebRtcPeerStatus::Recovering),
48        other => {
49            tracing::warn!(
50                discriminant = other,
51                "unknown dynclib WebRtcPeerStatus discriminant; dropping peer status"
52            );
53            None
54        }
55    }
56}
57
58#[cfg(test)]
59mod tests {
60    use super::*;
61
62    #[test]
63    fn peer_status_from_v1_drops_unknown_discriminants() {
64        use crate::guest::dynclib_abi::webrtc_peer_status as st;
65
66        assert_eq!(
67            peer_status_from_v1(st::IDLE),
68            Some(crate::WebRtcPeerStatus::Idle)
69        );
70        assert_eq!(
71            peer_status_from_v1(st::CONNECTING),
72            Some(crate::WebRtcPeerStatus::Connecting)
73        );
74        assert_eq!(
75            peer_status_from_v1(st::CONNECTED),
76            Some(crate::WebRtcPeerStatus::Connected)
77        );
78        assert_eq!(
79            peer_status_from_v1(st::RECOVERING),
80            Some(crate::WebRtcPeerStatus::Recovering)
81        );
82        assert_eq!(peer_status_from_v1(u32::MAX), None);
83    }
84}
85
86// The Component Model wasm runtime glue is gated on `not(feature = "web")`
87// so the `wasm32-unknown-unknown` + `web` target (which routes through
88// `actr-web-abi` instead) does not link the wit-bindgen host imports that
89// only resolve in a `wasm32-wasip2` Component environment.
90#[cfg(all(target_arch = "wasm32", not(feature = "web")))]
91pub mod wasm;
92
93#[cfg(feature = "cdylib")]
94pub mod dynclib;
95
96// Re-exports used by the `entry!` macro so macro expansions under a
97// user crate can reference adapter / binding items via a stable path
98// without having to know the internal module layout.
99#[cfg(all(target_arch = "wasm32", not(feature = "web")))]
100#[doc(hidden)]
101pub mod __wasm_macro_support {
102    // The `entry!` macro expands inside the user's crate, so every name
103    // it references must be reachable from an external crate path. Both
104    // the adapter helpers and the generated WIT types are re-exported
105    // here so the macro has a single stable `$crate::guest::__wasm_macro_support::*`
106    // prefix to poke at.
107    pub use super::wasm::adapter::{
108        WorkloadCell, run_dispatch, run_on_credential_expiring, run_on_credential_renewed,
109        run_on_data_stream, run_on_error, run_on_mailbox_backpressure, run_on_ready,
110        run_on_signaling_connected, run_on_signaling_connecting, run_on_signaling_disconnected,
111        run_on_start, run_on_stop, run_on_webrtc_connected, run_on_webrtc_connecting,
112        run_on_webrtc_disconnected, run_on_websocket_connected, run_on_websocket_connecting,
113        run_on_websocket_disconnected,
114    };
115    pub use super::wasm::generated::actr::workload::types::{
116        ActrError as WitActrError, ActrId as WitActrId, BackpressureEvent as WitBackpressureEvent,
117        CredentialEvent as WitCredentialEvent, DataStream as WitDataStream,
118        ErrorEvent as WitErrorEvent, PeerEvent as WitPeerEvent, RpcEnvelope as WitRpcEnvelope,
119    };
120    pub use super::wasm::generated::exports::actr::workload::workload::Guest;
121}
122
123/// Generate Component Model exports for a [`Workload`][crate::Workload]
124/// type.
125///
126/// Platform ABI is auto-selected by target:
127///
128/// - `#[cfg(all(target_arch = "wasm32", not(feature = "web")))]` — expands
129///   to an `impl Guest for __ActrEntryAdapter { ... }` bridging the user's
130///   [`Workload`][crate::Workload] into the `actr:workload/workload`
131///   export contract. The runtime calls `Dispatcher::dispatch` through
132///   the `dispatch` export and every observation hook through its
133///   matching WIT export.
134/// - `#[cfg(all(target_arch = "wasm32", feature = "web"))]` — expands to a
135///   `#[wasm_bindgen(start)]` bootstrap that wraps the user workload in a
136///   `WebWorkloadAdapter` and calls `actr_web_abi::host::register_workload`.
137///   Only the 17 `#[wasm_bindgen]` entry points generated inside
138///   `actr-web-abi::host` are exported to the Service Worker host.
139/// - `#[cfg(feature = "cdylib")]` — expands to the legacy
140///   `actr_init` / `actr_handle` / `actr_free_response` C-ABI exports
141///   used by native shared-library hosts.
142///
143/// # Arguments
144///
145/// - `$workload_type`: type implementing
146///   `actr_framework::Workload + Send + Sync + 'static`.
147/// - `$init_expr` (optional): expression returning a fresh instance of
148///   `$workload_type`. Defaults to `<$workload_type as Default>::default()`.
149///
150/// # Usage
151///
152/// ```rust,ignore
153/// use actr_framework::entry;
154///
155/// entry!(EchoServiceWorkload<MyService>);
156///
157/// // Or with a custom constructor:
158/// entry!(
159///     EchoServiceWorkload<MyService>,
160///     EchoServiceWorkload::new(MyService::new())
161/// );
162/// ```
163#[macro_export]
164macro_rules! entry {
165    // Single-argument form: default-construct the workload.
166    ($workload_type:ty) => {
167        $crate::entry!($workload_type, <$workload_type as ::core::default::Default>::default());
168    };
169
170    // Two-argument form: caller supplies the init expression.
171    ($workload_type:ty, $init_expr:expr) => {
172        // ── WASM Component Model exports ──────────────────────────────────
173        //
174        // wit-bindgen generates an `exports::actr::workload::workload::Guest`
175        // trait with 17 async methods (one `dispatch` + sixteen hooks). We
176        // emit a single zero-sized adapter struct in user-crate scope and
177        // route every method through helpers in `actr_framework::guest::wasm::adapter`.
178        //
179        // Skipped when the `web` feature is on — that path uses the
180        // wasm-bindgen + `actr-web-abi` pipeline below instead of the
181        // Component Model exports.
182        #[cfg(all(target_arch = "wasm32", not(feature = "web")))]
183        const _: () = {
184            // Module-local singleton cell. Lazy-init on first call; subsequent
185            // dispatches reuse the same instance.
186            static __ACTR_WORKLOAD: $crate::guest::__wasm_macro_support::WorkloadCell<$workload_type> =
187                $crate::guest::__wasm_macro_support::WorkloadCell::new();
188
189            fn __actr_workload() -> &'static $workload_type {
190                __ACTR_WORKLOAD.get_or_init(|| -> $workload_type { $init_expr })
191            }
192
193            struct __ActrEntryAdapter;
194
195            impl $crate::guest::__wasm_macro_support::Guest for __ActrEntryAdapter {
196                async fn dispatch(
197                    envelope: $crate::guest::__wasm_macro_support::WitRpcEnvelope,
198                ) -> ::core::result::Result<
199                    ::std::vec::Vec<u8>,
200                    $crate::guest::__wasm_macro_support::WitActrError,
201                > {
202                    $crate::guest::__wasm_macro_support::run_dispatch(
203                        __actr_workload(),
204                        envelope,
205                    )
206                    .await
207                }
208
209                async fn on_start() -> ::core::result::Result<
210                    (),
211                    $crate::guest::__wasm_macro_support::WitActrError,
212                > {
213                    $crate::guest::__wasm_macro_support::run_on_start(__actr_workload()).await
214                }
215
216                async fn on_ready() -> ::core::result::Result<
217                    (),
218                    $crate::guest::__wasm_macro_support::WitActrError,
219                > {
220                    $crate::guest::__wasm_macro_support::run_on_ready(__actr_workload()).await
221                }
222
223                async fn on_stop() -> ::core::result::Result<
224                    (),
225                    $crate::guest::__wasm_macro_support::WitActrError,
226                > {
227                    $crate::guest::__wasm_macro_support::run_on_stop(__actr_workload()).await
228                }
229
230                async fn on_error(
231                    event: $crate::guest::__wasm_macro_support::WitErrorEvent,
232                ) -> ::core::result::Result<
233                    (),
234                    $crate::guest::__wasm_macro_support::WitActrError,
235                > {
236                    $crate::guest::__wasm_macro_support::run_on_error(__actr_workload(), event)
237                        .await
238                }
239
240                async fn on_signaling_connecting() {
241                    $crate::guest::__wasm_macro_support::run_on_signaling_connecting(
242                        __actr_workload(),
243                    )
244                    .await
245                }
246
247                async fn on_signaling_connected() {
248                    $crate::guest::__wasm_macro_support::run_on_signaling_connected(
249                        __actr_workload(),
250                    )
251                    .await
252                }
253
254                async fn on_signaling_disconnected() {
255                    $crate::guest::__wasm_macro_support::run_on_signaling_disconnected(
256                        __actr_workload(),
257                    )
258                    .await
259                }
260
261                async fn on_websocket_connecting(
262                    event: $crate::guest::__wasm_macro_support::WitPeerEvent,
263                ) {
264                    $crate::guest::__wasm_macro_support::run_on_websocket_connecting(
265                        __actr_workload(),
266                        event,
267                    )
268                    .await
269                }
270
271                async fn on_websocket_connected(
272                    event: $crate::guest::__wasm_macro_support::WitPeerEvent,
273                ) {
274                    $crate::guest::__wasm_macro_support::run_on_websocket_connected(
275                        __actr_workload(),
276                        event,
277                    )
278                    .await
279                }
280
281                async fn on_websocket_disconnected(
282                    event: $crate::guest::__wasm_macro_support::WitPeerEvent,
283                ) {
284                    $crate::guest::__wasm_macro_support::run_on_websocket_disconnected(
285                        __actr_workload(),
286                        event,
287                    )
288                    .await
289                }
290
291                async fn on_webrtc_connecting(
292                    event: $crate::guest::__wasm_macro_support::WitPeerEvent,
293                ) {
294                    $crate::guest::__wasm_macro_support::run_on_webrtc_connecting(
295                        __actr_workload(),
296                        event,
297                    )
298                    .await
299                }
300
301                async fn on_webrtc_connected(
302                    event: $crate::guest::__wasm_macro_support::WitPeerEvent,
303                ) {
304                    $crate::guest::__wasm_macro_support::run_on_webrtc_connected(
305                        __actr_workload(),
306                        event,
307                    )
308                    .await
309                }
310
311                async fn on_webrtc_disconnected(
312                    event: $crate::guest::__wasm_macro_support::WitPeerEvent,
313                ) {
314                    $crate::guest::__wasm_macro_support::run_on_webrtc_disconnected(
315                        __actr_workload(),
316                        event,
317                    )
318                    .await
319                }
320
321                async fn on_credential_renewed(
322                    event: $crate::guest::__wasm_macro_support::WitCredentialEvent,
323                ) {
324                    $crate::guest::__wasm_macro_support::run_on_credential_renewed(
325                        __actr_workload(),
326                        event,
327                    )
328                    .await
329                }
330
331                async fn on_credential_expiring(
332                    event: $crate::guest::__wasm_macro_support::WitCredentialEvent,
333                ) {
334                    $crate::guest::__wasm_macro_support::run_on_credential_expiring(
335                        __actr_workload(),
336                        event,
337                    )
338                    .await
339                }
340
341                async fn on_mailbox_backpressure(
342                    event: $crate::guest::__wasm_macro_support::WitBackpressureEvent,
343                ) {
344                    $crate::guest::__wasm_macro_support::run_on_mailbox_backpressure(
345                        __actr_workload(),
346                        event,
347                    )
348                    .await
349                }
350
351                async fn on_data_stream(
352                    chunk: $crate::guest::__wasm_macro_support::WitDataStream,
353                    sender: $crate::guest::__wasm_macro_support::WitActrId,
354                ) -> ::core::result::Result<
355                    (),
356                    $crate::guest::__wasm_macro_support::WitActrError,
357                > {
358                    $crate::guest::__wasm_macro_support::run_on_data_stream(chunk, sender).await
359                }
360            }
361
362            $crate::guest::wasm::generated::export!(__ActrEntryAdapter with_types_in $crate::guest::wasm::generated);
363        };
364
365        // ── Web (wasm-bindgen + actr-web-abi) exports ─────────────────────
366        //
367        // Phase 6b: wrap the user workload in `WebWorkloadAdapter` and hand
368        // it to `actr_web_abi::host::register_workload` from a wasm-bindgen
369        // `start` hook. The 17 `#[wasm_bindgen]` entry points exported by
370        // `actr-web-abi::host` are the public surface the Service Worker
371        // dispatches into; they resolve through the registered adapter back
372        // into the user's `Workload` impl. See
373        // `bindings/web/docs/option-u-phase6-gamma-unified.zh.md` §4.5.
374        #[cfg(all(target_arch = "wasm32", feature = "web"))]
375        const _: () = {
376            // `wasm_bindgen(start)` functions are invoked once per module
377            // instantiation by the wasm-bindgen runtime. `register_workload`
378            // itself panics on double-registration, so wrapping it inside a
379            // `start` fn naturally enforces single-shot bootstrap.
380            //
381            // The attribute is referenced through its fully-qualified path
382            // so the user's crate does not need its own `wasm-bindgen`
383            // dependency — `actr-framework` re-exports the attribute through
384            // `web::__web_macro_support` under `feature = "web"`.
385            #[$crate::web::__web_macro_support::wasm_bindgen(start)]
386            fn __actr_web_bootstrap() {
387                let workload: $workload_type = $init_expr;
388                let adapter =
389                    $crate::web::__web_macro_support::WebWorkloadAdapter::new(workload);
390                $crate::web::__web_macro_support::register_workload(adapter);
391            }
392        };
393
394        // ── cdylib ABI exports ────────────────────────────────────────────
395        //
396        // Unchanged from pre-Phase-1; the Component Model rewrite is WASM-
397        // only and does not touch the native shared-library path.
398        #[cfg(feature = "cdylib")]
399        const _: () = {
400            static mut __ACTR_WORKLOAD: Option<$workload_type> = None;
401            static mut __ACTR_VTABLE: Option<*const $crate::guest::vtable::HostVTable> = None;
402
403            /// Initialize actor
404            ///
405            /// Host calls this after dlopen, passing HostVTable and init payload.
406            /// Returns 0 on success, negative on error.
407            /// Repeated calls return `INIT_FAILED` (one init per guest instance).
408            #[unsafe(no_mangle)]
409            pub unsafe extern "C" fn actr_init(
410                vtable: *const $crate::guest::vtable::HostVTable,
411                init_ptr: *const u8,
412                init_len: usize,
413            ) -> i32 {
414                if vtable.is_null() {
415                    return $crate::guest::dynclib_abi::code::INIT_FAILED;
416                }
417
418                let init_bytes = if init_ptr.is_null() || init_len == 0 {
419                    &[][..]
420                } else {
421                    unsafe { std::slice::from_raw_parts(init_ptr, init_len) }
422                };
423
424                // TODO: `actr_init` currently only validates that `InitPayloadV1`
425                // is decodable. The payload fields themselves are not yet
426                // consumed by the guest runtime on the dynclib path. This is a
427                // legacy gap carried forward from the previous init model.
428                if $crate::guest::dynclib_abi::decode_message::<$crate::guest::dynclib_abi::InitPayloadV1>(
429                    init_bytes,
430                )
431                .is_err()
432                {
433                    return $crate::guest::dynclib_abi::code::PROTOCOL_ERROR;
434                }
435
436                let workload: $workload_type = $init_expr;
437                unsafe {
438                    if __ACTR_WORKLOAD.is_some() {
439                        return $crate::guest::dynclib_abi::code::INIT_FAILED;
440                    }
441                    __ACTR_VTABLE = Some(vtable);
442                    __ACTR_WORKLOAD = Some(workload);
443                }
444                $crate::guest::dynclib_abi::code::SUCCESS
445            }
446
447            /// Handle one runtime ABI frame.
448            #[unsafe(no_mangle)]
449            pub unsafe extern "C" fn actr_handle(
450                req_ptr: *const u8,
451                req_len: usize,
452                resp_out: *mut *mut u8,
453                resp_len_out: *mut usize,
454            ) -> i32 {
455                use actr_protocol::prost::Message as ProstMessage;
456                use $crate::{MessageDispatcher, Workload};
457
458                // Get vtable
459                let vtable = match unsafe { __ACTR_VTABLE } {
460                    Some(vt) => vt,
461                    None => return $crate::guest::dynclib_abi::code::INIT_FAILED,
462                };
463
464                // Read runtime frame
465                if req_ptr.is_null() {
466                    return $crate::guest::dynclib_abi::code::PROTOCOL_ERROR;
467                }
468                let req_bytes = unsafe { std::slice::from_raw_parts(req_ptr, req_len) };
469
470                let frame = match $crate::guest::dynclib_abi::decode_message::<
471                    $crate::guest::dynclib_abi::AbiFrame,
472                >(req_bytes) {
473                    Ok(f) => f,
474                    Err(_) => return $crate::guest::dynclib_abi::code::PROTOCOL_ERROR,
475                };
476
477                if frame.op == $crate::guest::dynclib_abi::op::GUEST_LIFECYCLE {
478                    let payload = match <$crate::guest::dynclib_abi::GuestLifecycleV1 as $crate::guest::dynclib_abi::AbiPayload>::decode_payload(&frame.payload) {
479                        Ok(payload) => payload,
480                        Err(_) => return $crate::guest::dynclib_abi::code::PROTOCOL_ERROR,
481                    };
482
483                    let ctx = match unsafe {
484                        $crate::guest::dynclib::context::DynclibContext::from_invocation(vtable, payload.ctx)
485                    } {
486                        Ok(c) => c,
487                        Err(_) => return $crate::guest::dynclib_abi::code::HANDLE_FAILED,
488                    };
489
490                    let workload = unsafe {
491                        match __ACTR_WORKLOAD.as_ref() {
492                            Some(w) => w,
493                            None => return $crate::guest::dynclib_abi::code::INIT_FAILED,
494                        }
495                    };
496
497                    let lifecycle_result = match payload.hook {
498                        $crate::guest::dynclib_abi::lifecycle_hook::ON_START => {
499                            let fut = workload.on_start(&ctx);
500                            let waker = std::task::Waker::noop();
501                            let mut cx = std::task::Context::from_waker(waker);
502                            let mut pinned = std::pin::pin!(fut);
503                            match pinned.as_mut().poll(&mut cx) {
504                                std::task::Poll::Ready(v) => v,
505                                std::task::Poll::Pending => {
506                                    return $crate::guest::dynclib_abi::code::HANDLE_FAILED;
507                                }
508                            }
509                        }
510                        $crate::guest::dynclib_abi::lifecycle_hook::ON_READY => {
511                            let fut = workload.on_ready(&ctx);
512                            let waker = std::task::Waker::noop();
513                            let mut cx = std::task::Context::from_waker(waker);
514                            let mut pinned = std::pin::pin!(fut);
515                            match pinned.as_mut().poll(&mut cx) {
516                                std::task::Poll::Ready(v) => v,
517                                std::task::Poll::Pending => {
518                                    return $crate::guest::dynclib_abi::code::HANDLE_FAILED;
519                                }
520                            }
521                        }
522                        $crate::guest::dynclib_abi::lifecycle_hook::ON_STOP => {
523                            let fut = workload.on_stop(&ctx);
524                            let waker = std::task::Waker::noop();
525                            let mut cx = std::task::Context::from_waker(waker);
526                            let mut pinned = std::pin::pin!(fut);
527                            match pinned.as_mut().poll(&mut cx) {
528                                std::task::Poll::Ready(v) => v,
529                                std::task::Poll::Pending => {
530                                    return $crate::guest::dynclib_abi::code::HANDLE_FAILED;
531                                }
532                            }
533                        }
534                        _ => return $crate::guest::dynclib_abi::code::UNSUPPORTED_OP,
535                    };
536
537                    let resp_bytes = match lifecycle_result {
538                        Ok(()) => match $crate::guest::dynclib_abi::success_reply(::std::vec::Vec::new()) {
539                            Ok(bytes) => bytes,
540                            Err(code) => return code,
541                        },
542                        Err(err) => match $crate::guest::dynclib_abi::error_reply(
543                            $crate::guest::dynclib_abi::code::HANDLE_FAILED,
544                            err.to_string().into_bytes(),
545                        ) {
546                            Ok(bytes) => bytes,
547                            Err(code) => return code,
548                        },
549                    };
550
551                    let resp_len = resp_bytes.len();
552                    let layout = match std::alloc::Layout::from_size_align(resp_len.max(1), 1) {
553                        Ok(l) => l,
554                        Err(_) => return $crate::guest::dynclib_abi::code::GENERIC_ERROR,
555                    };
556                    let ptr = unsafe { std::alloc::alloc(layout) };
557                    if ptr.is_null() {
558                        return $crate::guest::dynclib_abi::code::GENERIC_ERROR;
559                    }
560
561                    unsafe {
562                        std::ptr::copy_nonoverlapping(resp_bytes.as_ptr(), ptr, resp_len);
563                        *resp_out = ptr;
564                        *resp_len_out = resp_len;
565                    }
566
567                    return $crate::guest::dynclib_abi::code::SUCCESS;
568                }
569
570                if frame.op == $crate::guest::dynclib_abi::op::GUEST_HOOK {
571                    let payload = match <$crate::guest::dynclib_abi::GuestHookV1 as $crate::guest::dynclib_abi::AbiPayload>::decode_payload(&frame.payload) {
572                        Ok(payload) => payload,
573                        Err(_) => return $crate::guest::dynclib_abi::code::PROTOCOL_ERROR,
574                    };
575
576                    let ctx = match unsafe {
577                        $crate::guest::dynclib::context::DynclibContext::from_invocation(vtable, payload.ctx)
578                    } {
579                        Ok(c) => c,
580                        Err(_) => return $crate::guest::dynclib_abi::code::HANDLE_FAILED,
581                    };
582
583                    let workload = unsafe {
584                        match __ACTR_WORKLOAD.as_ref() {
585                            Some(w) => w,
586                            None => return $crate::guest::dynclib_abi::code::INIT_FAILED,
587                        }
588                    };
589
590                    macro_rules! __actr_poll_unit {
591                        ($future:expr) => {{
592                            let fut = $future;
593                            let waker = std::task::Waker::noop();
594                            let mut cx = std::task::Context::from_waker(waker);
595                            let mut pinned = std::pin::pin!(fut);
596                            match pinned.as_mut().poll(&mut cx) {
597                                std::task::Poll::Ready(()) => {}
598                                std::task::Poll::Pending => {
599                                    return $crate::guest::dynclib_abi::code::HANDLE_FAILED;
600                                }
601                            }
602                        }};
603                    }
604
605                    let peer_event = |peer: $crate::guest::dynclib_abi::PeerEventV1| {
606                        $crate::PeerEvent {
607                            peer: peer.peer,
608                            relayed: peer.relayed,
609                            status: peer
610                                .status
611                                .and_then($crate::guest::peer_status_from_v1),
612                        }
613                    };
614
615                    let timestamp = |ts: $crate::guest::dynclib_abi::TimestampV1| {
616                        std::time::UNIX_EPOCH
617                            + std::time::Duration::new(ts.seconds, ts.nanoseconds)
618                    };
619
620                    match payload.hook {
621                        $crate::guest::dynclib_abi::runtime_hook::ON_SIGNALING_CONNECTING => {
622                            __actr_poll_unit!(workload.on_signaling_connecting(Some(&ctx)));
623                        }
624                        $crate::guest::dynclib_abi::runtime_hook::ON_SIGNALING_CONNECTED => {
625                            __actr_poll_unit!(workload.on_signaling_connected(Some(&ctx)));
626                        }
627                        $crate::guest::dynclib_abi::runtime_hook::ON_SIGNALING_DISCONNECTED => {
628                            __actr_poll_unit!(workload.on_signaling_disconnected(&ctx));
629                        }
630                        $crate::guest::dynclib_abi::runtime_hook::ON_WEBSOCKET_CONNECTING => {
631                            let event = match payload.peer {
632                                Some(peer) => peer_event(peer),
633                                None => return $crate::guest::dynclib_abi::code::PROTOCOL_ERROR,
634                            };
635                            __actr_poll_unit!(workload.on_websocket_connecting(&ctx, &event));
636                        }
637                        $crate::guest::dynclib_abi::runtime_hook::ON_WEBSOCKET_CONNECTED => {
638                            let event = match payload.peer {
639                                Some(peer) => peer_event(peer),
640                                None => return $crate::guest::dynclib_abi::code::PROTOCOL_ERROR,
641                            };
642                            __actr_poll_unit!(workload.on_websocket_connected(&ctx, &event));
643                        }
644                        $crate::guest::dynclib_abi::runtime_hook::ON_WEBSOCKET_DISCONNECTED => {
645                            let event = match payload.peer {
646                                Some(peer) => peer_event(peer),
647                                None => return $crate::guest::dynclib_abi::code::PROTOCOL_ERROR,
648                            };
649                            __actr_poll_unit!(workload.on_websocket_disconnected(&ctx, &event));
650                        }
651                        $crate::guest::dynclib_abi::runtime_hook::ON_WEBRTC_CONNECTING => {
652                            let event = match payload.peer {
653                                Some(peer) => peer_event(peer),
654                                None => return $crate::guest::dynclib_abi::code::PROTOCOL_ERROR,
655                            };
656                            __actr_poll_unit!(workload.on_webrtc_connecting(&ctx, &event));
657                        }
658                        $crate::guest::dynclib_abi::runtime_hook::ON_WEBRTC_CONNECTED => {
659                            let event = match payload.peer {
660                                Some(peer) => peer_event(peer),
661                                None => return $crate::guest::dynclib_abi::code::PROTOCOL_ERROR,
662                            };
663                            __actr_poll_unit!(workload.on_webrtc_connected(&ctx, &event));
664                        }
665                        $crate::guest::dynclib_abi::runtime_hook::ON_WEBRTC_DISCONNECTED => {
666                            let event = match payload.peer {
667                                Some(peer) => peer_event(peer),
668                                None => return $crate::guest::dynclib_abi::code::PROTOCOL_ERROR,
669                            };
670                            __actr_poll_unit!(workload.on_webrtc_disconnected(&ctx, &event));
671                        }
672                        $crate::guest::dynclib_abi::runtime_hook::ON_CREDENTIAL_RENEWED => {
673                            let event = match payload.credential {
674                                Some(credential) => $crate::CredentialEvent {
675                                    new_expiry: timestamp(credential.new_expiry),
676                                },
677                                None => return $crate::guest::dynclib_abi::code::PROTOCOL_ERROR,
678                            };
679                            __actr_poll_unit!(workload.on_credential_renewed(&ctx, &event));
680                        }
681                        $crate::guest::dynclib_abi::runtime_hook::ON_CREDENTIAL_EXPIRING => {
682                            let event = match payload.credential {
683                                Some(credential) => $crate::CredentialEvent {
684                                    new_expiry: timestamp(credential.new_expiry),
685                                },
686                                None => return $crate::guest::dynclib_abi::code::PROTOCOL_ERROR,
687                            };
688                            __actr_poll_unit!(workload.on_credential_expiring(&ctx, &event));
689                        }
690                        $crate::guest::dynclib_abi::runtime_hook::ON_MAILBOX_BACKPRESSURE => {
691                            let event = match payload.backpressure {
692                                Some(backpressure) => $crate::BackpressureEvent {
693                                    queue_len: backpressure.queue_len as usize,
694                                    threshold: backpressure.threshold as usize,
695                                },
696                                None => return $crate::guest::dynclib_abi::code::PROTOCOL_ERROR,
697                            };
698                            __actr_poll_unit!(workload.on_mailbox_backpressure(&ctx, &event));
699                        }
700                        _ => return $crate::guest::dynclib_abi::code::UNSUPPORTED_OP,
701                    }
702
703                    let resp_bytes = match $crate::guest::dynclib_abi::success_reply(::std::vec::Vec::new()) {
704                        Ok(bytes) => bytes,
705                        Err(code) => return code,
706                    };
707
708                    let resp_len = resp_bytes.len();
709                    let layout = match std::alloc::Layout::from_size_align(resp_len.max(1), 1) {
710                        Ok(l) => l,
711                        Err(_) => return $crate::guest::dynclib_abi::code::GENERIC_ERROR,
712                    };
713                    let ptr = unsafe { std::alloc::alloc(layout) };
714                    if ptr.is_null() {
715                        return $crate::guest::dynclib_abi::code::GENERIC_ERROR;
716                    }
717
718                    unsafe {
719                        std::ptr::copy_nonoverlapping(resp_bytes.as_ptr(), ptr, resp_len);
720                        *resp_out = ptr;
721                        *resp_len_out = resp_len;
722                    }
723
724                    return $crate::guest::dynclib_abi::code::SUCCESS;
725                }
726
727                if frame.op == $crate::guest::dynclib_abi::op::GUEST_DATA_STREAM {
728                    let payload = match <$crate::guest::dynclib_abi::GuestDataStreamV1 as $crate::guest::dynclib_abi::AbiPayload>::decode_payload(&frame.payload) {
729                        Ok(payload) => payload,
730                        Err(_) => return $crate::guest::dynclib_abi::code::PROTOCOL_ERROR,
731                    };
732
733                    let resp_bytes = match $crate::guest::dynclib::context::dispatch_registered_stream(payload) {
734                        Ok(()) => match $crate::guest::dynclib_abi::success_reply(::std::vec::Vec::new()) {
735                            Ok(bytes) => bytes,
736                            Err(code) => return code,
737                        },
738                        Err(err) => match $crate::guest::dynclib_abi::error_reply(
739                            $crate::guest::dynclib_abi::code::HANDLE_FAILED,
740                            err.to_string().into_bytes(),
741                        ) {
742                            Ok(bytes) => bytes,
743                            Err(code) => return code,
744                        },
745                    };
746
747                    let resp_len = resp_bytes.len();
748                    let layout = match std::alloc::Layout::from_size_align(resp_len.max(1), 1) {
749                        Ok(l) => l,
750                        Err(_) => return $crate::guest::dynclib_abi::code::GENERIC_ERROR,
751                    };
752                    let ptr = unsafe { std::alloc::alloc(layout) };
753                    if ptr.is_null() {
754                        return $crate::guest::dynclib_abi::code::GENERIC_ERROR;
755                    }
756
757                    unsafe {
758                        std::ptr::copy_nonoverlapping(resp_bytes.as_ptr(), ptr, resp_len);
759                        *resp_out = ptr;
760                        *resp_len_out = resp_len;
761                    }
762
763                    return $crate::guest::dynclib_abi::code::SUCCESS;
764                }
765
766                if frame.op != $crate::guest::dynclib_abi::op::GUEST_HANDLE {
767                    return $crate::guest::dynclib_abi::code::UNSUPPORTED_OP;
768                }
769
770                let handle = match <$crate::guest::dynclib_abi::GuestHandleV1 as $crate::guest::dynclib_abi::AbiPayload>::decode_payload(&frame.payload) {
771                    Ok(handle) => handle,
772                    Err(_) => return $crate::guest::dynclib_abi::code::PROTOCOL_ERROR,
773                };
774
775                let envelope = match actr_protocol::RpcEnvelope::decode(handle.rpc_envelope.as_slice()) {
776                    Ok(e) => e,
777                    Err(_) => return $crate::guest::dynclib_abi::code::PROTOCOL_ERROR,
778                };
779
780                let ctx = match unsafe {
781                    $crate::guest::dynclib::context::DynclibContext::from_invocation(vtable, handle.ctx)
782                } {
783                    Ok(c) => c,
784                    Err(_) => return $crate::guest::dynclib_abi::code::HANDLE_FAILED,
785                };
786
787                // Get workload reference
788                let workload = unsafe {
789                    match __ACTR_WORKLOAD.as_ref() {
790                        Some(w) => w,
791                        None => return $crate::guest::dynclib_abi::code::INIT_FAILED,
792                    }
793                };
794
795                // Route and execute via MessageDispatcher
796                type Dispatcher = <$workload_type as Workload>::Dispatcher;
797
798                // cdylib is native environment, can use tokio or synchronous execution
799                // Here we use the same single-threaded poll strategy as the old WASM path:
800                // All host callbacks (vtable function pointers) are synchronous, Future
801                // completes in one poll.
802                let resp_result = {
803                    let fut = Dispatcher::dispatch(workload, envelope, &ctx);
804                    let waker = std::task::Waker::noop();
805                    let mut cx = std::task::Context::from_waker(waker);
806                    let mut pinned = std::pin::pin!(fut);
807                    match pinned.as_mut().poll(&mut cx) {
808                        std::task::Poll::Ready(v) => v,
809                        std::task::Poll::Pending => {
810                            return $crate::guest::dynclib_abi::code::HANDLE_FAILED;
811                        }
812                    }
813                };
814
815                let resp_bytes = match resp_result {
816                    Ok(b) => match $crate::guest::dynclib_abi::success_reply(b.to_vec()) {
817                        Ok(bytes) => bytes,
818                        Err(code) => return code,
819                    },
820                    Err(err) => match $crate::guest::dynclib_abi::error_reply(
821                        $crate::guest::dynclib_abi::code::HANDLE_FAILED,
822                        err.to_string().into_bytes(),
823                    ) {
824                        Ok(bytes) => bytes,
825                        Err(code) => return code,
826                    },
827                };
828
829                // Allocate response buffer on guest heap
830                let resp_len = resp_bytes.len();
831                let layout = match std::alloc::Layout::from_size_align(resp_len.max(1), 1) {
832                    Ok(l) => l,
833                    Err(_) => return $crate::guest::dynclib_abi::code::GENERIC_ERROR,
834                };
835                let ptr = unsafe { std::alloc::alloc(layout) };
836                if ptr.is_null() {
837                    return $crate::guest::dynclib_abi::code::GENERIC_ERROR;
838                }
839
840                unsafe {
841                    std::ptr::copy_nonoverlapping(resp_bytes.as_ptr(), ptr, resp_len);
842                    *resp_out = ptr;
843                    *resp_len_out = resp_len;
844                }
845
846                $crate::guest::dynclib_abi::code::SUCCESS
847            }
848
849            /// Free guest-allocated response buffer
850            ///
851            /// Host calls this after using the response data returned by `actr_handle`.
852            #[unsafe(no_mangle)]
853            pub unsafe extern "C" fn actr_free_response(ptr: *mut u8, len: usize) {
854                if ptr.is_null() || len == 0 {
855                    return;
856                }
857                let layout = match std::alloc::Layout::from_size_align(len, 1) {
858                    Ok(l) => l,
859                    Err(_) => return,
860                };
861                unsafe { std::alloc::dealloc(ptr, layout) };
862            }
863        };
864    };
865}