Skip to main content

actr_framework/guest/
dynclib_abi.rs

1//! DynClib-only C ABI for actr workloads.
2//!
3//! This module is the handwritten C ABI used by the DynClib workload variant
4//! (loaded via dlopen). The WASM variant does NOT consume these types — it
5//! goes through wit-bindgen-generated code against `core/framework/wit/actr-workload.wit`.
6//!
7//! Do NOT add wasm-path code paths here. Do NOT reference this module from
8//! the wasm guest adapter. This module is kept in sync with the WIT contract
9//! by `tools/wit-lint`.
10
11use crate::Dest;
12use actr_protocol::prost::Message as ProstMessage;
13use actr_protocol::{ActrError, ActrId, ActrType, DataStream, PayloadType};
14
15/// ABI error codes.
16pub mod code {
17    /// Operation succeeded.
18    pub const SUCCESS: i32 = 0;
19    /// Generic unrecoverable error.
20    pub const GENERIC_ERROR: i32 = -1;
21    /// Initialization failed.
22    pub const INIT_FAILED: i32 = -2;
23    /// Message handling failed.
24    pub const HANDLE_FAILED: i32 = -3;
25    /// Memory allocation failed.
26    pub const ALLOC_FAILED: i32 = -4;
27    /// Protocol / codec error.
28    pub const PROTOCOL_ERROR: i32 = -5;
29    /// Guest-provided reply buffer is too small.
30    pub const BUFFER_TOO_SMALL: i32 = -6;
31    /// Unsupported runtime operation code.
32    pub const UNSUPPORTED_OP: i32 = -7;
33}
34
35/// Internal ABI version numbers.
36pub mod version {
37    /// ABI version 1.
38    pub const V1: u32 = 1;
39}
40
41/// Runtime operation codes carried inside [`AbiFrame`].
42pub mod op {
43    pub const HOST_CALL: u32 = 1;
44    pub const HOST_TELL: u32 = 2;
45    pub const HOST_CALL_RAW: u32 = 3;
46    pub const HOST_DISCOVER: u32 = 4;
47    pub const HOST_REGISTER_STREAM: u32 = 5;
48    pub const HOST_UNREGISTER_STREAM: u32 = 6;
49    pub const HOST_SEND_DATA_STREAM: u32 = 7;
50    pub const GUEST_HANDLE: u32 = 101;
51    pub const GUEST_DATA_STREAM: u32 = 102;
52    pub const GUEST_LIFECYCLE: u32 = 103;
53    pub const GUEST_HOOK: u32 = 104;
54}
55
56/// Lifecycle hook identifiers carried by [`GuestLifecycleV1`].
57pub mod lifecycle_hook {
58    pub const ON_START: u32 = 1;
59    pub const ON_READY: u32 = 2;
60    pub const ON_STOP: u32 = 3;
61}
62
63/// `WebRtcPeerStatus` discriminants carried by [`PeerEventV1::status`].
64/// One-to-one with `actr_framework::WebRtcPeerStatus`; 0-based to mirror
65/// the enum's declaration order. `optional` prost fields track presence, so
66/// `Some(IDLE)` is distinct from `None`.
67pub mod webrtc_peer_status {
68    pub const IDLE: u32 = 0;
69    pub const CONNECTING: u32 = 1;
70    pub const CONNECTED: u32 = 2;
71    pub const RECOVERING: u32 = 3;
72}
73
74/// Observation hook identifiers carried by [`GuestHookV1`].
75pub mod runtime_hook {
76    pub const ON_SIGNALING_CONNECTING: u32 = 1;
77    pub const ON_SIGNALING_CONNECTED: u32 = 2;
78    pub const ON_SIGNALING_DISCONNECTED: u32 = 3;
79    pub const ON_WEBSOCKET_CONNECTING: u32 = 4;
80    pub const ON_WEBSOCKET_CONNECTED: u32 = 5;
81    pub const ON_WEBSOCKET_DISCONNECTED: u32 = 6;
82    pub const ON_WEBRTC_CONNECTING: u32 = 7;
83    pub const ON_WEBRTC_CONNECTED: u32 = 8;
84    pub const ON_WEBRTC_DISCONNECTED: u32 = 9;
85    pub const ON_CREDENTIAL_RENEWED: u32 = 10;
86    pub const ON_CREDENTIAL_EXPIRING: u32 = 11;
87    pub const ON_MAILBOX_BACKPRESSURE: u32 = 12;
88}
89
90/// Dedicated payload used by `actr_init`.
91#[derive(Clone, PartialEq, prost::Message)]
92pub struct InitPayloadV1 {
93    #[prost(uint32, tag = "1")]
94    pub version: u32,
95    #[prost(string, tag = "2")]
96    pub actr_type: String,
97    #[prost(bytes = "vec", tag = "3")]
98    pub credential: Vec<u8>,
99    #[prost(bytes = "vec", tag = "4")]
100    pub actor_id: Vec<u8>,
101    #[prost(uint32, tag = "5")]
102    pub realm_id: u32,
103}
104
105/// Runtime frame used by both host->guest and guest->host invocation.
106///
107/// TODO: This type is temporarily `pub` because `actr_hyper` still performs
108/// cross-crate host-side ABI encoding and decoding through
109/// `actr_framework::guest::dynclib_abi`. Once the shared runtime ABI types
110/// are moved to a better ownership boundary, narrow this visibility to the
111/// intended internal-only surface.
112#[derive(Clone, PartialEq, prost::Message)]
113pub struct AbiFrame {
114    #[prost(uint32, tag = "1")]
115    pub abi_version: u32,
116    #[prost(uint32, tag = "2")]
117    pub op: u32,
118    #[prost(bytes = "vec", tag = "3")]
119    pub payload: Vec<u8>,
120}
121
122/// Runtime reply frame.
123///
124/// TODO: Keep visibility aligned with [`AbiFrame`]. This is public only as a
125/// temporary crate-topology workaround while host-side runtime code lives in
126/// `actr_hyper`.
127#[derive(Clone, PartialEq, prost::Message)]
128pub struct AbiReply {
129    #[prost(uint32, tag = "1")]
130    pub abi_version: u32,
131    #[prost(int32, tag = "2")]
132    pub status: i32,
133    #[prost(bytes = "vec", tag = "3")]
134    pub payload: Vec<u8>,
135}
136
137/// Invocation context injected by Hyper before entering guest handle logic.
138#[derive(Clone, PartialEq, prost::Message)]
139pub struct InvocationContextV1 {
140    #[prost(message, required, tag = "1")]
141    pub self_id: ActrId,
142    #[prost(message, optional, tag = "2")]
143    pub caller_id: Option<ActrId>,
144    #[prost(string, tag = "3")]
145    pub request_id: String,
146}
147
148/// Runtime host->guest handle payload.
149#[derive(Clone, PartialEq, prost::Message)]
150pub struct GuestHandleV1 {
151    #[prost(message, required, tag = "1")]
152    pub ctx: InvocationContextV1,
153    #[prost(bytes = "vec", tag = "2")]
154    pub rpc_envelope: Vec<u8>,
155}
156
157/// Runtime host->guest DataStream payload.
158#[derive(Clone, PartialEq, prost::Message)]
159pub struct GuestDataStreamV1 {
160    #[prost(message, required, tag = "1")]
161    pub chunk: DataStream,
162    #[prost(message, required, tag = "2")]
163    pub sender: ActrId,
164}
165
166/// Runtime host->guest lifecycle hook payload.
167#[derive(Clone, PartialEq, prost::Message)]
168pub struct GuestLifecycleV1 {
169    #[prost(message, required, tag = "1")]
170    pub ctx: InvocationContextV1,
171    #[prost(uint32, tag = "2")]
172    pub hook: u32,
173}
174
175/// Wall-clock timestamp represented as seconds + nanoseconds since Unix epoch.
176#[derive(Clone, PartialEq, prost::Message)]
177pub struct TimestampV1 {
178    #[prost(uint64, tag = "1")]
179    pub seconds: u64,
180    #[prost(uint32, tag = "2")]
181    pub nanoseconds: u32,
182}
183
184/// Peer-scoped event payload for WebSocket / WebRTC hooks.
185#[derive(Clone, PartialEq, prost::Message)]
186pub struct PeerEventV1 {
187    #[prost(message, required, tag = "1")]
188    pub peer: ActrId,
189    #[prost(bool, optional, tag = "2")]
190    pub relayed: Option<bool>,
191    /// `WebRtcPeerStatus` discriminant (see [`webrtc_peer_status`]).
192    /// `None` for WebSocket events.
193    #[prost(uint32, optional, tag = "3")]
194    pub status: Option<u32>,
195}
196
197/// Credential lifecycle event payload.
198#[derive(Clone, PartialEq, prost::Message)]
199pub struct CredentialEventV1 {
200    #[prost(message, required, tag = "1")]
201    pub new_expiry: TimestampV1,
202}
203
204/// Mailbox backpressure event payload.
205#[derive(Clone, PartialEq, prost::Message)]
206pub struct BackpressureEventV1 {
207    #[prost(uint64, tag = "1")]
208    pub queue_len: u64,
209    #[prost(uint64, tag = "2")]
210    pub threshold: u64,
211}
212
213/// Runtime host->guest observation hook payload.
214#[derive(Clone, PartialEq, prost::Message)]
215pub struct GuestHookV1 {
216    #[prost(message, required, tag = "1")]
217    pub ctx: InvocationContextV1,
218    #[prost(uint32, tag = "2")]
219    pub hook: u32,
220    #[prost(message, optional, tag = "3")]
221    pub peer: Option<PeerEventV1>,
222    #[prost(message, optional, tag = "4")]
223    pub credential: Option<CredentialEventV1>,
224    #[prost(message, optional, tag = "5")]
225    pub backpressure: Option<BackpressureEventV1>,
226}
227
228/// ABI-level destination encoding (replaces hand-rolled 0x00/0x01/0x02 byte protocol).
229#[derive(Clone, PartialEq, prost::Message)]
230pub struct DestV1 {
231    #[prost(oneof = "DestKind", tags = "1, 2, 3")]
232    pub kind: Option<DestKind>,
233}
234
235/// Destination variants carried inside [`DestV1`].
236#[derive(Clone, PartialEq, prost::Oneof)]
237pub enum DestKind {
238    #[prost(bool, tag = "1")]
239    Shell(bool),
240    #[prost(bool, tag = "2")]
241    Local(bool),
242    #[prost(message, tag = "3")]
243    Actor(ActrId),
244}
245
246impl DestV1 {
247    /// Construct a shell destination.
248    pub fn shell() -> Self {
249        Self {
250            kind: Some(DestKind::Shell(true)),
251        }
252    }
253
254    /// Construct a local destination.
255    pub fn local() -> Self {
256        Self {
257            kind: Some(DestKind::Local(true)),
258        }
259    }
260
261    /// Construct an actor destination.
262    pub fn actor(id: ActrId) -> Self {
263        Self {
264            kind: Some(DestKind::Actor(id)),
265        }
266    }
267
268    /// Convert the ABI destination into the framework destination.
269    pub fn try_into_dest(self) -> Result<Dest, ActrError> {
270        match self.kind {
271            Some(DestKind::Shell(_)) => Ok(Dest::Shell),
272            Some(DestKind::Local(_)) => Ok(Dest::Local),
273            Some(DestKind::Actor(id)) => Ok(Dest::Actor(id)),
274            None => Err(ActrError::DecodeFailure(
275                "destination kind is missing".into(),
276            )),
277        }
278    }
279}
280
281/// Runtime guest->host call payload.
282#[derive(Clone, PartialEq, prost::Message)]
283pub struct HostCallV1 {
284    #[prost(string, tag = "1")]
285    pub route_key: String,
286    #[prost(message, required, tag = "2")]
287    pub dest: DestV1,
288    #[prost(bytes = "vec", tag = "3")]
289    pub payload: Vec<u8>,
290}
291
292/// Runtime guest->host tell payload.
293#[derive(Clone, PartialEq, prost::Message)]
294pub struct HostTellV1 {
295    #[prost(string, tag = "1")]
296    pub route_key: String,
297    #[prost(message, required, tag = "2")]
298    pub dest: DestV1,
299    #[prost(bytes = "vec", tag = "3")]
300    pub payload: Vec<u8>,
301}
302
303/// Runtime guest->host raw call payload.
304#[derive(Clone, PartialEq, prost::Message)]
305pub struct HostCallRawV1 {
306    #[prost(string, tag = "1")]
307    pub route_key: String,
308    #[prost(message, required, tag = "2")]
309    pub target: ActrId,
310    #[prost(bytes = "vec", tag = "3")]
311    pub payload: Vec<u8>,
312}
313
314/// Runtime guest->host discovery payload.
315#[derive(Clone, PartialEq, prost::Message)]
316pub struct HostDiscoverV1 {
317    #[prost(message, required, tag = "1")]
318    pub target_type: ActrType,
319}
320
321/// Runtime guest->host DataStream registration payload.
322#[derive(Clone, PartialEq, prost::Message)]
323pub struct HostRegisterStreamV1 {
324    #[prost(string, tag = "1")]
325    pub stream_id: String,
326}
327
328/// Runtime guest->host DataStream unregistration payload.
329#[derive(Clone, PartialEq, prost::Message)]
330pub struct HostUnregisterStreamV1 {
331    #[prost(string, tag = "1")]
332    pub stream_id: String,
333}
334
335/// Runtime guest->host DataStream send payload.
336#[derive(Clone, PartialEq, prost::Message)]
337pub struct HostSendDataStreamV1 {
338    #[prost(message, required, tag = "1")]
339    pub dest: DestV1,
340    #[prost(message, required, tag = "2")]
341    pub chunk: DataStream,
342    #[prost(enumeration = "PayloadType", tag = "3")]
343    pub payload_type: i32,
344}
345
346/// Payloads that can automatically construct runtime frames.
347pub trait AbiPayload: ProstMessage + Default + Sized {
348    const ABI_VERSION: u32;
349    const OP: u32;
350
351    fn to_frame(&self) -> Result<AbiFrame, i32> {
352        let mut payload = Vec::new();
353        self.encode(&mut payload)
354            .map_err(|_| code::PROTOCOL_ERROR)?;
355
356        Ok(AbiFrame {
357            abi_version: Self::ABI_VERSION,
358            op: Self::OP,
359            payload,
360        })
361    }
362
363    fn decode_payload(bytes: &[u8]) -> Result<Self, i32> {
364        Self::decode(bytes).map_err(|_| code::PROTOCOL_ERROR)
365    }
366}
367
368impl AbiPayload for HostCallV1 {
369    const ABI_VERSION: u32 = version::V1;
370    const OP: u32 = op::HOST_CALL;
371}
372
373impl AbiPayload for HostTellV1 {
374    const ABI_VERSION: u32 = version::V1;
375    const OP: u32 = op::HOST_TELL;
376}
377
378impl AbiPayload for HostCallRawV1 {
379    const ABI_VERSION: u32 = version::V1;
380    const OP: u32 = op::HOST_CALL_RAW;
381}
382
383impl AbiPayload for HostDiscoverV1 {
384    const ABI_VERSION: u32 = version::V1;
385    const OP: u32 = op::HOST_DISCOVER;
386}
387
388impl AbiPayload for HostRegisterStreamV1 {
389    const ABI_VERSION: u32 = version::V1;
390    const OP: u32 = op::HOST_REGISTER_STREAM;
391}
392
393impl AbiPayload for HostUnregisterStreamV1 {
394    const ABI_VERSION: u32 = version::V1;
395    const OP: u32 = op::HOST_UNREGISTER_STREAM;
396}
397
398impl AbiPayload for HostSendDataStreamV1 {
399    const ABI_VERSION: u32 = version::V1;
400    const OP: u32 = op::HOST_SEND_DATA_STREAM;
401}
402
403impl AbiPayload for GuestHandleV1 {
404    const ABI_VERSION: u32 = version::V1;
405    const OP: u32 = op::GUEST_HANDLE;
406}
407
408impl AbiPayload for GuestDataStreamV1 {
409    const ABI_VERSION: u32 = version::V1;
410    const OP: u32 = op::GUEST_DATA_STREAM;
411}
412
413impl AbiPayload for GuestLifecycleV1 {
414    const ABI_VERSION: u32 = version::V1;
415    const OP: u32 = op::GUEST_LIFECYCLE;
416}
417
418impl AbiPayload for GuestHookV1 {
419    const ABI_VERSION: u32 = version::V1;
420    const OP: u32 = op::GUEST_HOOK;
421}
422
423/// Encode a protobuf message into bytes.
424pub fn encode_message<M: ProstMessage>(message: &M) -> Result<Vec<u8>, i32> {
425    let mut out = Vec::new();
426    message.encode(&mut out).map_err(|_| code::PROTOCOL_ERROR)?;
427    Ok(out)
428}
429
430/// Decode a protobuf message from bytes.
431pub fn decode_message<M: ProstMessage + Default>(bytes: &[u8]) -> Result<M, i32> {
432    M::decode(bytes).map_err(|_| code::PROTOCOL_ERROR)
433}
434
435/// Encode a successful runtime reply.
436pub fn success_reply(payload: Vec<u8>) -> Result<Vec<u8>, i32> {
437    encode_message(&AbiReply {
438        abi_version: version::V1,
439        status: code::SUCCESS,
440        payload,
441    })
442}
443
444/// Encode a failed runtime reply.
445pub fn error_reply(status: i32, message: impl Into<Vec<u8>>) -> Result<Vec<u8>, i32> {
446    encode_message(&AbiReply {
447        abi_version: version::V1,
448        status,
449        payload: message.into(),
450    })
451}
452
453// ─────────────────────────────────────────────────────────────────────────────
454// Shared guest-side helpers (used by both WASM and DynClib contexts)
455// ─────────────────────────────────────────────────────────────────────────────
456
457/// Convert a [`crate::Dest`] to the ABI-level [`DestV1`].
458pub fn dest_to_v1(dest: &crate::Dest) -> DestV1 {
459    match dest {
460        crate::Dest::Shell => DestV1::shell(),
461        crate::Dest::Local => DestV1::local(),
462        crate::Dest::Actor(id) => DestV1::actor(id.clone()),
463    }
464}
465
466/// Convert an ABI-level [`DestV1`] back to [`crate::Dest`].
467///
468/// Returns `None` if the `kind` field is absent.
469pub fn dest_v1_to_dest(v1: &DestV1) -> Option<crate::Dest> {
470    v1.clone().try_into_dest().ok()
471}
472
473/// Convert an ABI error code to an [`actr_protocol::ActrError`].
474pub fn abi_error_to_actr(code_val: i32) -> actr_protocol::ActrError {
475    use actr_protocol::ActrError;
476    match code_val {
477        code::GENERIC_ERROR => ActrError::Internal("host returned generic ABI error".into()),
478        code::INIT_FAILED => ActrError::Internal("host initialization failed".into()),
479        code::HANDLE_FAILED => ActrError::Internal("guest handle failed".into()),
480        code::ALLOC_FAILED => ActrError::Internal("memory allocation failed".into()),
481        code::PROTOCOL_ERROR => ActrError::DecodeFailure("ABI payload decode failed".into()),
482        code::BUFFER_TOO_SMALL => {
483            ActrError::Internal("reply buffer too small for host invoke".into())
484        }
485        code::UNSUPPORTED_OP => ActrError::NotImplemented("unsupported ABI operation".into()),
486        other => ActrError::Internal(format!("unexpected ABI status code {other}")),
487    }
488}
489
490/// Convert an [`AbiReply`] with a non-success status to an [`actr_protocol::ActrError`].
491pub fn reply_to_actr_error(reply: AbiReply) -> actr_protocol::ActrError {
492    use actr_protocol::ActrError;
493    if reply.payload.is_empty() {
494        return abi_error_to_actr(reply.status);
495    }
496
497    let message = String::from_utf8(reply.payload)
498        .unwrap_or_else(|_| format!("guest returned status {}", reply.status));
499
500    match reply.status {
501        code::PROTOCOL_ERROR => ActrError::DecodeFailure(message),
502        code::UNSUPPORTED_OP => ActrError::NotImplemented(message),
503        _ => ActrError::Internal(message),
504    }
505}