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