Skip to main content

ic_wire_endpoint/
lib.rs

1//! Typed IC entrypoints, owned by the independently released endpoint integration.
2//!
3//! Bare `query` and `update` derive an identity from the exported method name and
4//! use [`DEFAULT_REQUEST_LIMITS`] / [`DEFAULT_RESPONSE_LIMITS`]. Optional `id`,
5//! `request` and `response` constant expressions override those choices.
6//! A handler takes zero arguments (a framed unit request) or one owned Serde value.
7//! Synchronous handlers can select `borrow` with one request: borrowed fields and
8//! replies refer to Platform's argument vector, retained through reply encoding.
9//! This avoids a second field copy; the initial ingress copy is still required.
10//! Async handlers remain owned. Framing, budgets and validation are unchanged.
11//! `query(composite, ...)` selects a composite query. Async handlers use Platform's
12//! protected executor. `name = "..."` overrides the export name; `crate = "path"`
13//! selects a renamed/re-exported endpoint crate.
14//!
15//! Optional `contract = NAME` exposes a typed [`Contract`] constant taken from the
16//! exact signature and settings. Its native schema methods delegate to existing
17//! Wire projection; no automatic registry, custom Wasm metadata section or
18//! artifact admission is installed.
19//!
20//! Encoding failures trap, rolling back a synchronous update's mutations.
21//! Async updates follow ordinary IC transactions: state committed before an
22//! inter-canister await cannot be rolled back by a later reply-encoding trap.
23//! Only the current callback's changes roll back. Query state is not persisted.
24//!
25//! Frameworks may instead pass the helpers below to Platform's codec hooks around
26//! their existing guarded handlers. Admission must run as a Platform `guard`
27//! before its argument copy. Framework access refusals, dispatch, registration and
28//! final artifact checks remain framework-owned; do not stack two export macros.
29
30extern crate alloc;
31
32use alloc::{string::ToString, vec::Vec};
33#[cfg(all(feature = "typescript", not(target_family = "wasm")))]
34pub use ic_wire::typescript;
35pub use ic_wire::{Error, Limits, native};
36use serde::{Deserialize, Serialize};
37
38pub use ic_wire_derive::{query, update};
39
40/// Default full-frame request limits for the optional endpoint integration.
41///
42/// 64 KiB of frame bytes, 8 MiB conservative traversal allowance, 65,536 charged
43/// value visits and depth 32. The allocation allowance excludes the ingress copy;
44/// it is not a measured heap cap. Field bounds and application validation still
45/// apply. These are endpoint policy, not implicit limits in the core codec.
46pub const DEFAULT_REQUEST_LIMITS: Limits = Limits {
47    bytes: 64 * 1024,
48    allocation: 8 * 1024 * 1024,
49    nodes: 65_536,
50    depth: 32,
51};
52
53/// Default full-frame response limits, with the same values as the request defaults.
54/// Request and response overrides remain independent; reply failure still traps.
55pub const DEFAULT_RESPONSE_LIMITS: Limits = DEFAULT_REQUEST_LIMITS;
56
57/// A handler's types and explicit frame settings, without a registry or transport.
58///
59/// Prefer the attribute's `contract = NAME` output to a separately maintained
60/// declaration. Native schemas do not prove target-specific type-layout parity;
61/// the consumer still owns final artifact admission.
62pub struct Contract<Request, Response> {
63    /// IC method name.
64    pub name: &'static str,
65    /// IC mode: `query`, `composite_query`, or `update`.
66    pub mode: &'static str,
67    /// Native frame method identity.
68    pub id: u64,
69    /// Request limits, including the 11-byte frame header.
70    pub request: Limits,
71    /// Response limits, including the 11-byte frame header.
72    pub response: Limits,
73    types: core::marker::PhantomData<fn(Request) -> Response>,
74}
75
76impl<Request, Response> Contract<Request, Response> {
77    /// Check frame size, identity and direction before copying the full ingress.
78    /// Run as a Platform guard; payload validation still occurs during decoding.
79    pub fn admit_request(&self) {
80        admit_frame(self.id, self.request);
81    }
82
83    /// Construct the declaration used by the endpoint attribute.
84    #[doc(hidden)]
85    pub const fn new(
86        name: &'static str,
87        mode: &'static str,
88        id: u64,
89        request: Limits,
90        response: Limits,
91    ) -> Self {
92        Self {
93            name,
94            mode,
95            id,
96            request,
97            response,
98            types: core::marker::PhantomData,
99        }
100    }
101}
102
103#[cfg(all(feature = "typescript", not(target_family = "wasm")))]
104impl<Request: crate::typescript::Wire, Response: crate::typescript::Wire>
105    Contract<Request, Response>
106{
107    /// Project the actual request type using the existing native schema owner.
108    pub fn request_schema(&self) -> Result<serde_json::Value, crate::typescript::GenerationError> {
109        crate::typescript::native_schema::<Request>()
110    }
111
112    /// Project the actual response type using the existing native schema owner.
113    pub fn response_schema(&self) -> Result<serde_json::Value, crate::typescript::GenerationError> {
114        crate::typescript::native_schema::<Response>()
115    }
116}
117
118/// Platform paths used by generated entrypoints, including renamed Wire imports.
119#[doc(hidden)]
120pub mod __private {
121    pub use ic_platform::cdk;
122}
123
124/// Trap on an oversized ingress before Platform allocates/copies argument bytes.
125pub fn admit_request(limits: Limits) {
126    admit_request_len(limits);
127}
128
129fn admit_request_len(limits: Limits) -> usize {
130    // This moves Canic's pre-copy check to Wire without changing its acceptance
131    // rule: the full native frame counts toward bytes, including its 11-byte header.
132    // Consumers retain their existing method IDs and limits; no wire migration.
133    let length = ic_platform::ic0::msg_arg_data_size();
134    if length > limits.bytes {
135        ic_platform::cdk::api::trap("Wire request exceeds byte allowance");
136    }
137    length
138}
139
140#[cfg_attr(target_arch = "wasm32", inline(never))]
141fn admit_frame(id: u64, limits: Limits) {
142    let length = admit_request_len(limits);
143    if length < native::REQUEST_HEADER_BYTES {
144        trap_error(crate::Error::Malformed);
145    }
146    let mut header = [0; native::REQUEST_HEADER_BYTES];
147    ic_platform::ic0::msg_arg_data_copy(&mut header, 0);
148    // Previously identity/direction were checked after copying the complete
149    // ingress. Check the same eleven bytes first, using the same validator.
150    // Bytes and accepted frames are unchanged; no consumer migration. Payload
151    // validation remains mandatory, including the existing decoder header check.
152    native::validate_request_header(id, &header).unwrap_or_else(|error| trap_error(error));
153}
154
155/// Decode a complete native request frame, trapping on typed codec failure.
156///
157/// Call [`admit_request`] before obtaining the argument buffer. This function
158/// preserves native identity, traversal, primitive and complete-input validation.
159/// Borrowed fields refer to `bytes`; the caller must retain it through their use.
160pub fn decode_request<'de, T: Deserialize<'de>>(bytes: &'de [u8], id: u64, limits: Limits) -> T {
161    native::decode_request(id, bytes, limits).unwrap_or_else(|error| trap_error(error))
162}
163
164/// Encode the native response frame within its allowances, trapping on failure.
165///
166/// A trap (rather than a rejection or error reply) preserves synchronous mutation
167/// rollback. Earlier async transactions remain committed; see the module docs.
168pub fn encode_response<T: Serialize + ?Sized>(value: &T, id: u64, limits: Limits) -> Vec<u8> {
169    native::encode_response(id, value, limits).unwrap_or_else(|error| trap_error(error))
170}
171
172// Keep failure formatting shared across request/reply types and admission.
173#[cold]
174#[inline(never)]
175fn trap_error(error: crate::Error) -> ! {
176    ic_platform::cdk::api::trap(error.to_string())
177}