iClaustron training for Rust developers · module 9 of 9 · 20 min
Inside the crates
How iClaustron is built, for Rust developers who will read, debug or change it: the rules, the crates, the receive path in code, the C ABI, and the tools that check it all.
The rules · chapter 07, the style guide
C-like Rust: what the style guide allows and what it forbids
Allowed
- Structs, enums,
implmethods,match, plain loops. Result<T, IcError>and?on every fallible call.Box,Vec,Arc; a mutex at a documented level.unsafefor FFI, libc and words as bytes: small, commented.
Forbidden in library code
async: explicit threads andpollare the design.- Trait bounds,
where,impl Trait; traits beyond the derives. - Closures, iterator chains, macros beyond a sanctioned few.
unwrapoutside tests;Rc,RefCell; shadowing.
The test for every construct: can a C programmer who has read one page of Rust syntax follow it without looking anything up?
In the crates today: no trait is defined and nothing is async. Closures do appear: thread bodies, catch_unwind in ic_capi, the key walks of hash.rs.
iclaustron: doc/rust/07-style-guide.md (Allowed and encouraged; Forbidden in library code); doc/rust/01-goals-and-principles.md (principle 2); crates/ic_util/src/threadpool.rs (ThreadFunc); crates/ic_capi/src/lib.rs (guarded, on_conn); crates/ic_apid/src/hash.rs (KeyWalk)
The rules · chapter 09, licensing
Clean room: written from the wire, MIT licensed, no RonDB name
- Listen: capture the traffic, read it with the project's own Wireshark dissector.
- Ask the author what the wire does not show.
- Read RonDB's source only to settle a fact, never to copy.
- Write the fact into chapter 05, in our words and names.
- Implement from chapter 05; verify against a live cluster.
- The crates are MIT. A file translated from legacy-c/ also carries iClaustron AB's copyright line; legacy-c/ stays GPLv2, never built.
- Every signal and block is named by us,
IC_SIG_KEY_OP_REQ = 12; comments cite chapter 05:Verify: chapter 05 §17.5. cargo xtask namesreads RonDB's names from a RonDB tree when it runs and looks for each in crates/, tools/, xtask/, include/ and doc/.- One crate from crates.io:
libc. deny.toml lists the licences allowed.
iclaustron: doc/rust/09-licensing.md (Facts; Clean-room procedure; Names; Repository); crates/ic_signals/src/numbers.rs (IC_SIG_KEY_OP_REQ); xtask/src/names.rs (module note, SCOPE); Cargo.lock; deny.toml; legacy-c/README.md
The crates · from crates/*/Cargo.toml
The crates' dependencies, as their Cargo.toml files declare them
A row is one Cargo.toml: a filled square is declared and used, a dashed one declared only. Every mark lies right of the line: no cycle.
Every other crate uses ic_port. libc, the workspace's one crate from crates.io, is used by ic_port and ic_comm alone.
ic_apid uses five crates and declares a sixth; ic_capi uses ic_apid, ic_signals, ic_port. ic_signals and ic_apic do not use each other.
Two declarations go unused in src/: ic_comm's ic_util and ic_apid's ic_protocol.
iclaustron: crates/*/Cargo.toml ([dependencies]); Cargo.toml ([workspace.dependencies]); Cargo.lock (libc); uses counted with grep '<crate>::' in each crate's src/
The crates · where to start reading
What each crate holds, its size, and where to start reading
| Crate | Lines | What it holds | Start at |
|---|---|---|---|
| ic_port | 4 340 | the OS layer, glib replaced: libc calls, IcMutex, time, IcError, NDB codes, tracing | sync.rs, err.rs |
| ic_util | 2 649 | PtrArray, bitmap, memory container, connectstring, thread pool, MD5, XXH3 | ptr_array.rs |
| ic_comm | 2 330 | a TCP connection, a poll set over epoll or kqueue, the line protocol, a page pool | connection.rs |
| ic_protocol | 484 | base64 and the management protocol's words | proto_str.rs |
| ic_apic | 2 583 | the management client, the configuration blob, the typed configuration | mgm_client.rs |
| ic_signals | 8 528 | signal and block numbers, the header, codecs per signal family, program words | header.rs, key_ops.rs |
| ic_apid | 31 473 | the Data API: threads, connections, transactions, scans, executors, programs | lib.rs, apid_conn.rs |
| ic_capi | 7 511 | the C ABI in one file: 179 exported functions | lib.rs, its opening note |
Lines are wc -l of src/, tests included. Each lib.rs names its modules. MODULE.md maps a crate to its C; ic_apic's, ic_signals' and ic_apid's are still the phase 0 template.
iclaustron: crates/*/src/lib.rs (module notes); crates/*/MODULE.md; crates/ic_capi/src/lib.rs (#[no_mangle] exports); wc -l crates/*/src/*.rs
ic_port · err.rs and ndb_err.rs
IcError is one i32 code; category and severity come from it
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[repr(C)]
pub struct IcError {
pub code: i32,
}
impl IcError {
pub fn category(&self) -> ErrorCategory {
if self.is_ic_error() {
return ic_class(self.code).0;
}
crate::ndb_err::class_of(self.code).category()
}
// ... severity(), is_temporary(), message()
- Every fallible call returns
Result<T, IcError>; the C ABI hands C the code itself. - Three spaces in one
i32: 7000 to 7166 are iClaustron's, small positive codes the OS's errno, the rest NDB's. ic_classsorts iClaustron's codes;ndb_err.rsholds NDB's classes. 7129, a panic, is Internal and Stop.- It lives in ic_port, below ic_util: the port layer's own calls fail with it.
iclaustron: crates/ic_port/src/err.rs (IcError, impl IcError, ic_class, IC_FIRST_ERROR, IC_LAST_ERROR, ErrorCategory, ErrorSeverity); ndb_err.rs (ErrorClass, class_of); crates/ic_port/src/lib.rs (module note)
ic_port · sync.rs
IcMutex owns its data and a level; debug builds check the order
pub struct IcMutex<T> {
level: u32,
inner: Mutex<T>,
}
fn check_order(level: u32) {
if level == IC_MUTEX_LEVEL_UNORDERED || !cfg!(debug_assertions) {
return;
}
// ... violated: a level this thread holds, >= level
if violated != 0 {
// ... print both levels
std::process::abort();
}
}
- The value lives inside:
lock()gives a guard, the only way to it, released when dropped.IcCond::waittakes the guard and gives it back. - Each lock pushes its level on a thread-local stack. Locking at or below a level held aborts a debug build; a release build skips the check.
- Levels run from 1, a table's AUTO_INCREMENT range, to 11, a completion slot; 0 is never checked. Module 02 has the table.
iclaustron: crates/ic_port/src/sync.rs (IcMutex, check_order, push_level, IcMutexGuard, IcCond::wait, IC_MUTEX_LEVEL_*); crates/ic_port/MODULE.md (Deviations; Rust notes for C readers)
ic_util · ptr_array.rs and mc.rs
PtrArray ids: a 24-bit slot and a 40-bit generation, a 32-bit word on the wire
pub fn remove(&mut self, id: PtrId) -> Option<T> {
let index = id.index() as usize;
let slot = self.slots.get_mut(index)?;
if slot.generation == IC_RETIRED || slot.generation != id.generation() {
return None;
}
let value = slot.value.take()?;
// ...
// A slot whose generation would wrap is retired, never used again, ...
if slot.generation >= IC_MAX_GENERATION {
slot.generation = IC_RETIRED;
return Some(value);
}
slot.generation += 1;
self.free_list.push(index as u32);
Some(value)
}
- An id is
index | generation << 24in au64. Generations start at 1, so a valid id is never 0,PtrId::NONE, which means no object. - A slot whose generation would wrap is retired: no id ever names a second object. Slots are reused last freed, first.
- A signal carries
wire_word(), the index and eight bits of the generation;id_of_wirefinds the live object again, and a reply is checked against its transaction's id too. MemoryContainerhands out handles, not pointers: buffer, offset, length. No other crate uses it today.
iclaustron: crates/ic_util/src/ptr_array.rs (module note; PtrId, wire_word, PtrArray::insert, remove, id_of_wire); mc.rs (McHandle); crates/ic_util/MODULE.md (Deviations); crates/ic_capi/src/lib.rs (opening note)
ic_signals · the wire format
ic_signals is the wire alone: no sockets, no threads, no state
/// The eight words of signal data. The column data length, third, is
/// zero: in the long form it is the length of section 1.
pub fn encode(&self) -> [u32; IC_KEY_OP_REQ_LEN] {
[
self.coord_ptr,
self.query_ref,
0,
self.table_id,
self.flags_word,
self.table_version,
self.trans_id_lo,
self.trans_id_hi,
]
}
- A request encodes to a fixed array,
[u32; N].header::decodegives aMessage<'_>that borrows the words it read. - 16 modules on ic_port alone; 131 unit tests run them with no cluster, MySQL's own words from captures among them.
- Fuzz targets for its decoders are planned in chapter 08; none is in the tree yet.
iclaustron: crates/ic_signals/src/key_ops.rs (KeyOpReq::encode); header.rs (decode, Message); lib.rs (module note); interp.rs and scans.rs (tests); doc/rust/08-testing-and-tooling.md (Unit tests)
ic_apid · the module map
ic_apid's 32 files, grouped by the job they do
| Job | Files | Read first |
|---|---|---|
| Threads and links | apid_global, rec_thread, send_pool, heartbeat, connect_thread, node_connect, node_state, handshake, adaptive_send | apid_global.rs: start_with_options |
| Receive path | signal_reader, signal_page, thread_conn, fragments | thread_conn.rs, its module note |
| The connection | apid_conn, transaction, query, key_op | apid_conn.rs: poll_with_requests |
| Tables and rows | dict_client, dict_cache, record, row_codec, hash, collation_client, collation_map, text_row, auto_increment | record.rs, then row_codec.rs |
| Scans | scan, index_scan, index_order, scan_reader | scan.rs, its module note |
| Executors, programs | executor, program | executor.rs: Runner::run |
Every file opens with a //! note; a translated one names its legacy-c/ file. The largest: scan.rs, 3 442 lines; apid_conn.rs, 2 580.
iclaustron: crates/ic_apid/src/lib.rs (module list); the module note of each file; wc -l crates/ic_apid/src/*.rs
ic_apid · receive path
The receive path in code: reader, router, page, inbox, poll
SignalReader::read_from reads a socket's bytes straight into the u32 words of a receive page, never swapped, so reading a word costs nothing.
route_signals decodes each header where it lies and routes by receiver block: a small signal is copied, a large one held in its page.
A SignalPage gathers one user thread's signals for the round: four words of header each, then the data or where it lies.
Router::post_all ends the round: ThreadConnection::post swaps the page into the inbox under one lock, then wakes the thread.
poll_with_requests takes the pages, walks each with signal_page::next and hands every SignalView to receive_view.
iclaustron: crates/ic_apid/src/signal_reader.rs (SignalReader::read_from, ReceivePage, IC_RECEIVE_PAGE_WORDS); rec_thread.rs (route_signals); signal_page.rs (module note, next); thread_conn.rs (Router::post_all, ThreadConnection::post); apid_conn.rs (poll_with_requests, receive_view)
ic_apid · node_connect.rs and apid_conn.rs
SignalView borrows its words: poll reads signals where they lie
pub struct SignalView<'a> {
pub sig_no: u16,
// ... the two blocks, the sending node, fragment_state
pub data: &'a [u32],
pub sections: [&'a [u32]; header::IC_MAX_SECTIONS],
pub num_sections: usize,
}
for page in &pages {
let mut at: usize = 0;
while let Some(signal) = signal_page::next(page, &mut at) {
taken += 1;
self.receive_view(&signal);
}
}
- A
SignalViewisCopy: a few numbers and slices into the page. Decoding a reply reads the words in place. - Only what must outlive the page is copied, with
to_owned: a fragment waiting for its train, a reply a request waits for. - The pages go back to the inbox on the next poll, emptied; a page keeps its allocation, sized by the largest round.
iclaustron: crates/ic_apid/src/node_connect.rs (SignalView, to_owned); apid_conn.rs (poll_with_requests, receive_view); signal_page.rs (module note, next); thread_conn.rs (exchange)
ic_apid · apid_conn.rs
ApidConnection: one thread's objects, held with no lock
pub struct ApidConnection {
pub(crate) shared: Arc<ApidShared>,
inbox: Arc<ThreadConnection>,
expectations: Expectations,
coord_records: Vec<CoordRecord>,
free_coord: Vec<Vec<u32>>,
pub(crate) queries: PtrArray<ApidQuery>,
pub(crate) transactions: PtrArray<Transaction>,
pub(crate) executed: VecDeque<QueryId>,
active: Vec<Option<TransId>>,
outgoing: Vec<Outgoing>,
pub(crate) scans: PtrArray<Scan>,
pub(crate) receivers: PtrArray<ReceiverSlot>,
// ...
- One thread uses it, so none of its 49 fields is locked. Only the inbox is shared, with the receive threads.
- Queries, transactions, scans and receivers sit in
PtrArrays, named by ids; coordinator records at fixed places,active[]by the same place. - Requests packed at define wait per node in
Outgoinguntil the send. An executor'sRunnerowns an ApidConnection like any user thread.
iclaustron: crates/ic_apid/src/apid_conn.rs (ApidConnection, CoordRecord, Outgoing, Staged, coord_index_of); executor.rs (Runner, Runner::run)
ic_apid · row_codec.rs and hash.rs
row_codec and hash: a row into words, a key onto a node
row_codec.rs: a record's row and the words
key_info_into: each key column in attribute order, padded to whole words.read_attr_info: every column, 0xFFF0, or a bitmask of them, 0xFFF3.unpack_row: a result bitmap, then the values; a NULL takes no room.
hash.rs: a key, its partition, a node
distribution_key: its columns padded to words, a character column by its collation.partition_hash: MD5's second word for a table from before 22.10.1, else XXH3's high half.choose_node: a read-backup table's started replicas in turn, else the primary.
Both work on plain slices, rows as &[u8] and words as &[u32]. A wrong node costs a hop in the cluster, never a wrong answer.
iclaustron: crates/ic_apid/src/row_codec.rs (module note, key_info_into, read_attr_info, unpack_row, IC_COL_READ_EVERY, IC_COL_READ_MASKED); hash.rs (module note, distribution_key, partition_hash, choose_node, primary_of); crates/ic_util/src/lib.rs (md5, xxh3)
unsafe · counted with grep
Where unsafe lives: counted per crate, with what it is for
| Crate | unsafe blocks | Where, and what for |
|---|---|---|
| ic_capi | 375 | C's pointers: handles, out pointers, sized structs, callbacks. 174 of the 179 exports are unsafe extern "C" fn, with 32 unsafe helpers. |
| ic_comm | 19 | connection.rs, 11: socket, bind, connect, socket options, poll, recv; poll_set.rs, 8: epoll and kqueue |
| ic_port | 13 | daemon.rs, 8: fork, signal handlers, _exit; socket.rs, 4: fcntl, close; time.rs, 1: getrusage |
| ic_apid | 3 | words seen as bytes: signal_reader.rs, 2, to read a socket into a page; node_connect.rs, 1, to write words out |
| the other four | 0 | ic_signals, ic_util, ic_protocol, ic_apic |
410 blocks, 410 SAFETY: comments. The row path, record.rs, row_codec.rs and query.rs, has no unsafe block: a row is the query's own Vec<u8>.
iclaustron: grep 'unsafe {' and 'SAFETY' in crates/*/src; crates/ic_apid/src/signal_reader.rs (bytes_of, bytes_of_mut); node_connect.rs (words_as_bytes); query.rs (module note); doc/rust/07-style-guide.md (unsafe blocks)
ic_capi · the C ABI
ic_capi exports: a Rust object boxed as a handle, or named by id
#[no_mangle]
pub unsafe extern "C" fn ic_apid_global_create_connection(
global: *mut IcApidGlobal,
conn: *mut *mut IcApidConnection,
) -> i32 {
// ... the null checks, and g: the global
guarded(|| match g.create_connection() {
Ok(made) => {
let handle = Box::into_raw(Box::new(made)) as *mut IcApidConnection;
// SAFETY: checked non-null above; the caller's promise.
unsafe { conn.write(handle) };
0
}
Err(e) => e.code,
})
IC_APID_CONNECTIONis an opaque struct; the handle is the boxedApidConnection, given back toBox::from_rawbyic_apid_conn_free.- Queries, transactions, scans and scan batches cross as their 64-bit ids, never 0;
ic_apid_scan_batch_scan(conn, batch)asks the connection for a batch's scan. - cbindgen writes include/iclaustron/ic_apid.h from lib.rs and cbindgen.toml;
cargo xtask headerdiffs it with the copy checked in.
iclaustron: crates/ic_capi/src/lib.rs (ic_apid_global_create_connection, ic_apid_conn_free, IcApidConnection, guarded); crates/ic_capi/cbindgen.toml; crates/ic_capi/MODULE.md (How a call crosses); xtask/src/main.rs (run_header)
ic_capi · panics and sizes
The C boundary's guards: panics become 7129, short structs refused
unsafe fn on_conn<T, F: FnOnce(&mut ApidConnection) -> T>(
conn: *mut IcApidConnection,
f: F,
) -> Result<T, i32> {
let raw = conn as *mut ApidConnection;
// ... a null handle: IC_ERROR_NULL_POINTER
let c = unsafe { &mut *raw };
if c.is_unusable() {
return Err(err::IC_ERROR_INTERNAL_PANIC);
}
match catch_unwind(AssertUnwindSafe(|| f(c))) {
Ok(value) => Ok(value),
Err(_) => {
unsafe { (*raw).set_unusable() };
Err(err::IC_ERROR_INTERNAL_PANIC)
- 134 of the 179 exports run under
catch_unwind, throughon_connon a connection orguarded; the other 45 are getters, setters and_inits. - A panic may leave a connection half changed, so it is marked unusable: every later call but its free returns 7129.
sized_from_creads an argument struct only if its size field covers this header's struct, else 7020; options are read field by field,get_sized.
iclaustron: crates/ic_capi/src/lib.rs (on_conn, guarded, sized_from_c, get_sized, put_sized); crates/ic_apid/src/apid_conn.rs (set_unusable, is_unusable); crates/ic_capi/cbindgen.toml (Panics; Structs with a size)
Testing · chapter 08, as built
The test pyramid: unit tests, captured words, a live cluster, fuzzing
539 unit tests in the crates' src/, 251 in ic_apid and 134 in ic_signals, run by cargo test with no cluster.
Captured words: what MySQL sent, from tcpdump captures read with the dissector, asserted word for word, the pcap and frame named.
116 integration tests, 108 in ic_apid and 8 through the C ABI, against a live cluster; without IC_TEST_CONNECTSTRING each returns at once.
Fuzzing: nine fuzz_ tests, #[ignore]d by cargo test, give mutated seeds to the decoders of what comes from the network through ic_port::fuzz; a case fails on a panic, an overflow or a second's run.
iclaustron: grep '#[test]' in crates/*/src; crates/ic_apid/tests/integration.rs and crates/ic_capi/tests/c_api.rs (module notes); crates/ic_signals/src/interp.rs (tests), fuzz_tests.rs; crates/ic_port/src/fuzz.rs; tools/scan_capture.sh; xtask/src/main.rs (run_test_integration, run_fuzz); doc/rust/08-testing-and-tooling.md
Tooling · xtask/ and the files at the root
cargo xtask and the checked-in configs: the checks before a commit
cargo xtask check # fmt, clippy, test, style cargo xtask style # 80 columns, forbidden constructs cargo xtask header # cbindgen, diffed with the header cargo xtask test-integration # against a live cluster cargo xtask fuzz [target] # the decoders' fuzz tests cargo xtask names [rondb-tree] # no RonDB name in the tree cargo xtask install [prefix] # library, header, pkg-config cargo xtask tags # vim tags, Rust and legacy-c/
checkrunscargo fmt --check, clippy with-D warnings,cargo test --workspace, thenstyle.stylelooks in code only forasync fn,Rc<,RefCell<,where,.unwrap()and six more; style-allow.txt names 5 exceptions.- rust-toolchain.toml takes the stable channel, rustfmt.toml 80 columns and 2 spaces; the workspace lints deny
missing_safety_doc.
iclaustron: xtask/src/main.rs (print_help, run_check, FORBIDDEN, run_style, run_fuzz); xtask/style-allow.txt; rust-toolchain.toml; rustfmt.toml; Cargo.toml ([workspace.lints], [profile.fuzz]); clippy.toml; deny.toml
Tooling · tools/ic_bench and tools/ic_model
ic_bench measures, ic_model predicts a change before it is built
ic_bench: measure
- Primary key reads or updates: threads, batches in flight, or callers through executors.
- Built with
count-allocsit counts allocations: 5 per 1 000 reads, one a batch, the benchmark's own. The library's path allocates nothing per read.
ic_model: predict
- A discrete-event model: user threads, receive threads, data nodes, each cost in ns.
- Calibrated on six measured runs: within 14 % in rate, 22 % in latency.
- Set what a change would move, sweep it, and see which server reaches 1.
Both on the author's Mac with two release data nodes on it: allocations counted 2026-10-14, 100 000 keys; the model fitted 2026-09-24.
iclaustron: tools/ic_bench/src/main.rs (module note); alloc_count.rs; doc/rust/06-phases.md (Phase 7, the performance pass); tools/perf_pass.sh; tools/ic_model/src/main.rs; doc/rust/12-performance-model.md (Calibration; Using it)
History · chapter 06, phases
How it was built: translated foundations, then new code by phase
| Phase | What it built | Mostly |
|---|---|---|
| 0 | legacy-c/ set aside, MIT, empty crates, the C header rewritten first | bootstrap |
| 1–2 | ic_port, ic_util, ic_comm, ic_protocol; ic_apic, the configuration | translated from the C |
| 3–5 | threads, membership, node failure; dictionary, records; transactions, key operations | new code |
| 6 | our own names, the Wireshark dissector, unique keys, every row size, 4012 | new code |
| 7 | the C ABI, the user guide, the examples in Rust and C: release 0.1 | packaging |
| 8–10 | executors, scans, character sets; programs, AUTO_INCREMENT; scans completed | new code, 0.2 and 0.3 |
| 11 | hardening, and what early applications need | open |
Phases 0 to 5, sized at 20 weeks, were built from 2026-09-18 to 2026-09-25. A translated module keeps the C's file and function names.
iclaustron: doc/rust/06-phases.md (phase table; Replanned 2026-10-08; Working method per module); doc/rust/01-goals-and-principles.md (principles 1 and 11); git log
History · from legacy-c/ to the crates
A worked mapping: post_ndb_messages in C, ThreadConnection::post
C · legacy-c/api/ic_apid_rec_thread.ic
Rust · crates/ic_apid/src/thread_conn.rs
ic_mutex_lock(loc_thd_conn->mutex);
/* Link it into list on this thread connection object */
/* ... */
loc_thd_conn->last_received_message= last_ndb_message_page;
if (loc_thd_conn->thread_wait_cond)
{
loc_thd_conn->thread_wait_cond= FALSE;
signal_flag= TRUE;
}
ic_mutex_unlock(loc_thd_conn->mutex);
/* END Critical Section with delayed wake up signal */
if (signal_flag)
{
ic_cond_signal(loc_thd_conn->cond);
}
let mut inbox = self.inbox.lock();
if inbox.closed {
page.clear();
return false;
}
let fresh = inbox.spare.pop().unwrap_or_default();
let full = std::mem::replace(page, fresh);
inbox.pages.push(full);
let wake = inbox.waiting;
inbox.waiting = false;
// Unlock, and only then wake; see the note at the top of the file.
drop(inbox);
if wake {
self.cond.signal();
}
The same order: lock, append, note a sleeper, unlock, then wake. The unlock is drop(inbox); the list of pages is a Vec, a spare swapped in.
iclaustron: legacy-c/api/ic_apid_rec_thread.ic (post_ndb_messages); crates/ic_apid/src/thread_conn.rs (module note, ThreadConnection::post); doc/rust/11-c-to-rust-mappings.md (Threads and synchronisation); doc/rust/03-module-map.md
Recap · module 09
Inside the crates: what to take away
- C-like Rust, from the wire in a clean room: no trait of our own, nothing async.
- Eight crates, no cycle: the rest all use ic_port, ic_signals nothing else; libc is the one from outside.
- Errors are one
i32; mutexes carry levels; queries, transactions, scans are ids with a generation. - Replies go reader, router, page, inbox, poll, and are read in place as a
SignalView. - unsafe sits at the edges, 410 blocks with as many SAFETY notes; the row path has none.
- The C ABI boxes objects as handles and turns a panic into 7129.
iclaustron: doc/rust/07-style-guide.md; doc/rust/09-licensing.md; crates/*/Cargo.toml; crates/ic_port/src (err.rs, sync.rs); crates/ic_util/src/ptr_array.rs; crates/ic_apid/src; crates/ic_capi/src/lib.rs
iClaustron training for Rust developers · 9 modules · you are at 09