thruput_sdk/interface_table.rs
1//! The interface table: which interfaces this contract implements, collected
2//! by the linker from its `#[contract]` impl blocks.
3//!
4//! Every `#[contract] impl Trait for T` (and every bare `#[contract] impl T`)
5//! expands to one 8-byte [`InterfaceId`] record via
6//! [`__interface_record!`](crate::__interface_record). [`implements!`](crate::implements)
7//! answers whether a given id is among them. This is what `thruput-erc165-if`
8//! builds `supports_interface` on: the claim is read off the impls that
9//! actually linked, so there is no list to maintain and nothing to get wrong.
10//!
11//! # Layout and cost
12//!
13//! On the guest the records use the entrypoint table's shape: `#[used]`
14//! statics in the `thruput_rv32_interfaces` input section, which the
15//! runtime linker script `KEEP`s and brackets as its own output section.
16//! Whether that section reaches the deployed image is the lowerer's call,
17//! made the way it already drops the raw spec section: the reader below
18//! touches a one-word anchor in `thruput_rv32_interfaces_anchor`, an
19//! ordinary GC-able section that survives the link only if the table reader
20//! behind [`implements!`](crate::implements) was instantiated. The lowerer copies the table into VM memory only when
21//! the anchor section is present. A contract with no ERC-165 impl therefore
22//! ships an image byte-identical to one built without this module; one with
23//! an impl pays four bytes of anchor plus 8 per impl block, plus the scan.
24//!
25//! # What counts as "this contract"
26//!
27//! The unit is the contract image, not the storage struct. A crate with
28//! `impl X for A` and `impl Y for B` links both into one image whose
29//! dispatch table answers to both interfaces at one address, so its table
30//! holds both ids and `supports_interface` says yes to both — which is the
31//! truth ERC-165 asks about. On the guest the table is simply the whole
32//! image. On the host, where one test process holds many contracts, the
33//! records go through a `linkme` slice keyed by the package that expanded
34//! them, name and version both, so [`implements!`](crate::implements) from
35//! inside a contract crate sees every impl block of that crate — `A`'s and
36//! `B`'s alike, the same as the guest — while another contract crate's
37//! records, or another version of this one (a migration test may link two),
38//! stay apart. That is the scope the entrypoint guards and the mock
39//! registry already use for "one contract".
40
41use crate::InterfaceId;
42
43/// One host-side record: the package (name and version) whose `#[contract]`
44/// impl block expanded it, and the interface that block implements.
45#[cfg(not(target_arch = "riscv32"))]
46#[doc(hidden)]
47pub struct HostRecord {
48 pub package: &'static str,
49 pub version: &'static str,
50 pub id: InterfaceId,
51}
52
53#[cfg(not(target_arch = "riscv32"))]
54#[doc(hidden)]
55#[crate::__linkme::distributed_slice]
56#[linkme(crate = crate::__linkme)]
57pub static HOST_RECORDS: [HostRecord];
58
59/// True if the contract identified by `package`/`version` carries a
60/// `#[contract]` impl of the interface identified by `interface_id`.
61///
62/// [`implements!`](crate::implements) supplies the caller crate's own
63/// package name and version, which is the form contracts use. On the guest
64/// there is one contract per image, so the scope is not consulted and the
65/// table is scanned whole; on the host the records are filtered to that
66/// package. Neither touches storage.
67#[cfg(target_arch = "riscv32")]
68pub fn implements_in(
69 _package: &str,
70 _version: &str,
71 interface_id: InterfaceId,
72) -> bool {
73 rv32::contains(interface_id)
74}
75
76/// True if the contract identified by `package`/`version` carries a
77/// `#[contract]` impl of the interface identified by `interface_id`.
78///
79/// [`implements!`](crate::implements) supplies the caller crate's own
80/// package name and version, which is the form contracts use. On the guest
81/// there is one contract per image, so the scope is not consulted and the
82/// table is scanned whole; on the host the records are filtered to that
83/// package. Neither touches storage.
84#[cfg(not(target_arch = "riscv32"))]
85pub fn implements_in(
86 package: &str,
87 version: &str,
88 interface_id: InterfaceId,
89) -> bool {
90 HOST_RECORDS.iter().any(|r| {
91 r.id == interface_id && r.package == package && r.version == version
92 })
93}
94
95/// True if this contract carries a `#[contract]` impl of the interface
96/// identified by the given [`InterfaceId`].
97///
98/// Expands to [`interface_table::implements_in`](crate::interface_table::implements_in)
99/// scoped to the invoking crate's package name and version, so it must be
100/// invoked from the contract crate itself — which is where a
101/// `supports_interface` body lives:
102///
103/// ```ignore
104/// fn supports_interface(id: InterfaceId) -> bool {
105/// thruput_sdk::implements!(id)
106/// }
107/// ```
108#[macro_export]
109macro_rules! implements {
110 ($id:expr) => {
111 $crate::interface_table::implements_in(
112 env!("CARGO_PKG_NAME"),
113 env!("CARGO_PKG_VERSION"),
114 $id,
115 )
116 };
117}
118
119#[cfg(target_arch = "riscv32")]
120mod rv32 {
121 // Bracket symbols for the `thruput_rv32_interfaces` output section,
122 // defined by link/thruput-rv32.ld in the rv32-runtime crate. Declared
123 // as records so `&raw const` yields correctly-typed pointers; only their
124 // addresses are read.
125 unsafe extern "C" {
126 static __thruput_rv32_interfaces_start: super::InterfaceId;
127 static __thruput_rv32_interfaces_end: super::InterfaceId;
128 }
129
130 // The lowerer's signal that the table has a reader. Deliberately not
131 // `#[used]`: the linker keeps this static only because `contains` reads
132 // it, so its section exists in the ELF exactly when `implements` was
133 // instantiated, and the lowerer keys the table's presence in the image
134 // off that. Section name shared with the linker script and the
135 // CLI's rv32 lowerer (`INTERFACE_TABLE_ANCHOR_SECTION`).
136 #[unsafe(link_section = "thruput_rv32_interfaces_anchor")]
137 static ANCHOR: u32 = 1;
138
139 /// Scan the table for `wanted`. Out of line and cold so LLVM keeps it
140 /// a plain eight-byte-stride loop: inlined at -O3 it unrolls the scan
141 /// eightfold, ~250 bytes of code for a table of a handful of records.
142 #[cold]
143 #[inline(never)]
144 pub(super) fn contains(wanted: super::InterfaceId) -> bool {
145 // SAFETY: the linker script ALIGN(8)s the section start and brackets
146 // exactly the KEEP'd `thruput_rv32_interfaces` input section, whose
147 // sole writer is `__interface_record!` (each entry a `static [u8;
148 // 8]`, so the section is a well-formed array of stride 8). An empty
149 // table gives start == end and no reads. The bracket symbols are
150 // distinct declarations, so they are compared as addresses rather
151 // than through `offset_from`, which needs shared provenance.
152 unsafe {
153 core::ptr::read_volatile(&raw const ANCHOR);
154 let mut p = &raw const __thruput_rv32_interfaces_start;
155 let end = &raw const __thruput_rv32_interfaces_end;
156 while (p as usize) < (end as usize) {
157 if core::ptr::read_volatile(p) == wanted {
158 return true;
159 }
160 p = p.add(1);
161 }
162 false
163 }
164 }
165}
166
167/// Register one interface-table record for the invoking crate. Emitted by
168/// `#[contract]` for every impl block; not for hand-written use.
169#[doc(hidden)]
170#[macro_export]
171macro_rules! __interface_record {
172 ($id:expr) => {
173 const _: () = {
174 #[cfg(target_arch = "riscv32")]
175 #[used]
176 #[unsafe(link_section = "thruput_rv32_interfaces")]
177 static __THRUPUT_INTERFACE_RECORD: $crate::InterfaceId = $id;
178
179 #[cfg(not(target_arch = "riscv32"))]
180 #[$crate::__linkme::distributed_slice(
181 $crate::interface_table::HOST_RECORDS
182 )]
183 #[linkme(crate = $crate::__linkme)]
184 static __THRUPUT_INTERFACE_RECORD:
185 $crate::interface_table::HostRecord =
186 $crate::interface_table::HostRecord {
187 package: env!("CARGO_PKG_NAME"),
188 version: env!("CARGO_PKG_VERSION"),
189 id: $id,
190 };
191 };
192 };
193}