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