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