Repository navigation
Add craft-plugin-sdk and craft-plugin-sdk-macros: the guest SDK for craft-abi 1 plugins - #455
bigwolfeman wants to merge 62 commits into
Conversation
…raft apps Adds craft-command and its #[command] macro crate, craft-command-derive. craft-command holds a registry of commands that can be added and removed at run time, before/after hooks with per-owner time budgets, a change bus with a Tracked<T> wrapper, a bounded observer queue, JSON Schema from params docs, a JSON diff, and (feature testing) a conformance harness for apps. Part of storytold#414. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NFCDiYoKS325v2rtv2F7D8
Adds craft-ipc, the transport for a Craft app that talks to other Craft apps on one machine. It is native only: interprocess has no wasm32 build. - server and client: one JSON object per line, push events, a bulk connection for frames, token auth on every connection, connection and time limits. - discovery: one record per running app in a per-user runtime directory, with a lock file that shows whether the owner is alive. - launch, trust, cache: find an app binary, start it only after an executable check, keep caches in a private per-user directory. - activate: focus hand-off between apps. - moniker, record, payload, asset, refresh, fetch, watch: asset names, stored link records, open payloads, linked-file polling and a frame cache. - PROTOCOL.md describes the wire protocol. The record schemas and fixtures are in the crate. - The crate compiles and passes clippy -D warnings on x86_64-pc-windows-gnu and aarch64-apple-darwin. This is a compile and lint fix only. No one ran the Windows or macOS code paths, so this does not claim they work there. Part of storytold#414. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NFCDiYoKS325v2rtv2F7D8
… gate Adds craft-plugin-abi. It describes a WebAssembly plugin package and decides whether the package may load, without running guest code. - manifest: the craft.manifest custom section, its parser and JSON Schema. - gate: check_package, the install-time policy check (size, memory, tables, functions, start function, imports, export signatures, feature allow list). - permissions, ops, descriptor, format, text: permission names, host call numbers and status codes, the craft_process descriptor, the format contribution shapes, and the rules for text a plugin supplies. - ABI.md lists the rules the crate encodes. Part of storytold#414. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NFCDiYoKS325v2rtv2F7D8
ActivationBackend gains can_apply_token (default false). On Linux a token only counts when the backend can apply it to a live window. Stock winit cannot, so the answer is false plus a request for user attention. Wire values are unchanged. PROTOCOL.md says what each value means. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NFCDiYoKS325v2rtv2F7D8
… plugins Adds craft-plugin-sdk and craft-plugin-sdk-macros. They are the guest side of craft-plugin-abi. - host: call_json and typed helpers over craft_host.call (log, progress, cmd, state, doc, clock, random, link). - entry, ui: handler request and response types, and the panel tree builder. - rt, backend: the runtime behind the exports. The wasm32 modules are the only code that uses unsafe, each behind an explicit allow. Everything else is denied. - testing: a fake host, so handlers run in a plain cargo test. - #[craft_plugin(manifest = "craft.toml")] checks the manifest with the install gate's parser at compile time and emits the five ABI exports on wasm32. Part of storytold#414. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NFCDiYoKS325v2rtv2F7D8
|
Important Review skippedToo many files! This PR contains 152 files, which is 52 over the limit of 100. To get a review, reduce the PR to 100 files or fewer by splitting it into smaller PRs or changing its base branch. Upgrade to a paid plan to raise the limit. This review couldn't start because sufficient usage credits or metered capacity aren't available. Add credits or update usage-based reviews in the billing tab, then retry.
|
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NFCDiYoKS325v2rtv2F7D8
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NFCDiYoKS325v2rtv2F7D8
- ChangeBus: each subscriber queue holds at most 256 changes. A full queue drops the oldest change and counts it. `drain_with_lost` returns the count; `drain` is unchanged. - ObserverQueue: the coalesce window is kept per id in `last_at`, so `drain` no longer resets it. The per-push scan of the call deque is gone. - json_diff: `Op.value` deserializes a present `"value": null` as `Some(Null)`, so a set to null stays a set after a serde round trip. An absent member stays `None`. - json_diff::patch: ops run on a copy, and the copy replaces the document only when every op succeeds. A failed patch leaves the document unchanged. - Interceptor::before takes `&Call`, not `&mut Call`. A gate can no longer edit params, id, origin or depth outside the verdict path, so the panic and budget rules cover every change. A compile_fail doctest pins this. Tests and docs follow the new signature. - Conformance T6: new `CommandHost::take_plugin_gate_ids`. T6 now checks that the plugin's own gate sees a probe command before unregister and sees nothing after it. It no longer relies on the host's veto-all gate. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NFCDiYoKS325v2rtv2F7D8
- manifest: a format contribution needs fs.picked in permissions.required or permissions.optional. Manifest::validate and the JSON Schema both enforce it. - gate: legacy v1 modules now run the feature allow list and module validation. - gate: the memory maximum uses checked_shl and saturating_mul. The reader already refuses an exponent of 64 or more, so this is hardening only. - format: SizeMismatch Display computes the byte count in u128. It overflowed u64 for u32::MAX by u32::MAX. - docs: ABI.md and README.md list what legacy modules skip and keep, and the format permission rule. - tests: overflow, page size, legacy feature, and fs.picked cases. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NFCDiYoKS325v2rtv2F7D8
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NFCDiYoKS325v2rtv2F7D8
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NFCDiYoKS325v2rtv2F7D8
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NFCDiYoKS325v2rtv2F7D8
…ssages A table with no declared maximum can grow past the 100,000 element cap through table.grow, so the gate now rejects it (TableMaxMissing), on both the native and the legacy path. Violation messages repeat import names, op names and serde text from the package, so Display now applies the same text rules as manifest text. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NFCDiYoKS325v2rtv2F7D8
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NFCDiYoKS325v2rtv2F7D8 # Conflicts: # Cargo.lock
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NFCDiYoKS325v2rtv2F7D8 # Conflicts: # Cargo.lock
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NFCDiYoKS325v2rtv2F7D8 # Conflicts: # Cargo.lock
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NFCDiYoKS325v2rtv2F7D8
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NFCDiYoKS325v2rtv2F7D8
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NFCDiYoKS325v2rtv2F7D8
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NFCDiYoKS325v2rtv2F7D8
The abi gate now requires fs.picked for a format contribution. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NFCDiYoKS325v2rtv2F7D8
pull_line dropped the bytes it had read when a read timed out, so next_event returned Ok(None) and the next read saw only the tail of the line. The client now keeps the partial line and resumes it on the next read. A test splits an event line across a timeout. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NFCDiYoKS325v2rtv2F7D8
… newline At end of stream LineReader::read returned a buffered line with no length check, so a peer that sent one byte over the limit and closed its write side got that line dispatched, while the same bytes with a newline were refused. The end of stream path now returns TooLarge for a line over the limit. A test covers both paths. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NFCDiYoKS325v2rtv2F7D8
…o_large write_value passed every reply to write_line, which has no size check. A sink result larger than MAX_LINE went out as one line that the client refuses, and the rest of the line stayed unread on the connection. Replies now go through wire::write_reply_line. A reply over the limit is replaced by a payload_too_large error that keeps the request id. When that does not fit either, or the value has no id, nothing is written and the request loop ends. Event lines are unchanged. wire gains write_reply_line. write_line behaves as before. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NFCDiYoKS325v2rtv2F7D8
The test expected the first notice to come from a shallow poll. With a 50 ms deep interval the poll that first sees the edit can be a deep one, so the assertion failed on a slow or loaded machine. It now takes the deep flag from the notice and checks every other field. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NFCDiYoKS325v2rtv2F7D8
FrameLayout::check only tested len == stride * h, so w = 4, stride = 1, h = 3, len = 3 was a valid rgba8 layout. A reader that splits the payload by stride cannot decode it. frames.open, a rendered frame and the client reply parser now require stride >= w * bytes per pixel (4 for rgba8_premul, 8 for rgba16f_premul). open checks before it mints a ticket, so a refused layout leaves no ticket. The bulk test source used stride 16 for rgba16f with w = 4, which needs 32. It now uses a stride that fits the format. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NFCDiYoKS325v2rtv2F7D8
…ed field valid_rfc3339 checked the day only against 1 to 31, so 2026-02-31T09:30:00Z passed LinkRecord::validate while the schema's date-time format refused it. The year, month and day now go through chrono::NaiveDate::from_ymd_opt, as the workspace rules require for calendar logic. The accepted syntax is unchanged. The schema agreement table gets rows for impossible days and for leap days. craft-ipc gains the workspace chrono dependency. chrono is already in Cargo.lock; the lock file gains one dependency line for craft-ipc. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NFCDiYoKS325v2rtv2F7D8
The asset.moniker pattern accepted any id without / ? #, so a record the schema passed could fail LinkRecord::validate (bad or truncated escape, raw space, escape that decodes to invalid UTF-8, id over 512 bytes). The pattern is now two patterns under allOf. One checks the shape: unreserved characters and two-digit escapes only, 1 to 512 of them, which is 1 to 512 decoded bytes. The other checks that the decoded bytes are valid UTF-8, written as the well-formed UTF-8 byte sequences. Neither uses lookahead, so any ECMA 262 engine can run them. The schema agreement test gets rows for each case in the list above and for the length limits, and a sweep of lead, second and tail bytes that compares the schema verdict with the Rust verdict. This tightens the published schema: documents it accepted that Rust refused are now refused. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NFCDiYoKS325v2rtv2F7D8
Server::shutdown and drop join every connection thread, and an Assets call runs on one with no deadline and no view of the stop flag. The trait docs now say so and ask implementations to bound their own I/O. Behavior is unchanged. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NFCDiYoKS325v2rtv2F7D8
Clippy's type_complexity lint failed on the test helper's return type. Behavior is unchanged. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NFCDiYoKS325v2rtv2F7D8
…ith a deadline Server::shutdown and drop join every connection thread, and an Assets or Activate call ran on one with no deadline and no view of the stop flag, so a provider that never returned made shutdown hang. These calls now run on a helper thread (server/provider.rs). The connection thread waits for the answer in 100 ms slices, for at most Limits::provider_deadline (default REPLY_DEADLINE_SECS) and not past the stop flag. On timeout the caller gets a timeout reply and the helper is detached, so shutdown does not join it. At most 32 helper threads are alive at once, stuck ones included, and a call past that gets a busy error without starting a thread. A panic in a provider becomes an io_error reply. Frames stays on the connection thread because it is one call per frame. Its trait docs now say that the stop flag is checked before each request and that a running call is waited for. The module docs describe the shutdown guarantee for each provider kind. Public API: new Limits::provider_deadline field. Tests use a provider that blocks until released, for Assets and for Activate. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NFCDiYoKS325v2rtv2F7D8
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NFCDiYoKS325v2rtv2F7D8
When the fetch queue was full, the oldest ask was dropped but its waiting callers got no answer. They woke, found the key neither cached nor in flight, and queued it again, which dropped another ask and repeated the cycle. A dropped ask now records a failure for its key before the waiters are woken, so each waiter returns Fetched::Failed and does not queue the key again. The drop-oldest policy and stats.skipped are unchanged. Failure records are shared with the worker path and kept to a fixed number, oldest first. No type or variant changes. The Fetched::Failed doc now covers this case. A test overflows the queue while a caller waits on the oldest key, and checks that the caller returns at once and that no further asks are dropped. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NFCDiYoKS325v2rtv2F7D8
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NFCDiYoKS325v2rtv2F7D8
percent_decode used u8::from_str_radix, which accepts a leading plus, so file:///tmp/a%+fb decoded to the byte 0x0f and went on to the media probe instead of getting payload_invalid. The two bytes after a percent sign must now both be ASCII hex digits. The decode test gets a plus, a minus, a space and a short escape. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NFCDiYoKS325v2rtv2F7D8
… macOS path test On Unix systems other than Linux, current_euid read the owner of a file named .craft-uid-<pid> in the temp directory, made with create and truncate. A file or symlink planted at that name was opened and truncated. The probe now lives in probe_euid: random name, create_new (which refuses an existing path), mode 0600, and removal before it returns. The std library has no safe euid call and this crate forbids unsafe code, so the probe stays. A test plants a file and a symlink at the old and a similar name and checks that both are untouched and that the probe file is gone. macos_fallback_is_per_user compares Unix paths, so it and the macos_runtime_path helper it needs are now compiled for tests on Unix only. That removes the unused function warning in Windows test builds. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NFCDiYoKS325v2rtv2F7D8
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NFCDiYoKS325v2rtv2F7D8
…arker Server::emit wrote the serialized event unchecked. A line over MAX_LINE made Client::next_event return TooLarge and left the stream out of sync. enqueue now checks the serialized size and queues the existing overflow marker instead. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NFCDiYoKS325v2rtv2F7D8
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NFCDiYoKS325v2rtv2F7D8
# Conflicts: # Cargo.lock
# Conflicts: # Cargo.lock
check_package gathered facts before the size check, so a package over the cap with many craft.manifest sections still allocated an entry for each and could trigger the feature probes. An oversized package now returns PackageTooLarge alone, before any parsing.
A module that mixed ec_, pc_ and vc_ exports returned before the table and memory checks, so the author had to resubmit to learn about a table with no maximum. The caps now run first. The doc comment on check_package no longer claims an oversized package costs no allocation; it skips parsing and validation.
# Conflicts: # Cargo.toml # README.md
FredApps
left a comment
There was a problem hiding this comment.
Scoped review of SDK runtime/host-call helpers and macro wiring at 6729c002141627b6b454747ee498d5f341559cfc; inherited dependencies are outside this review. No local build or wasm runtime tests run; current checks do not establish passing validation.
[P2] Do not expose raw deallocation through a safe public Rust function. rt.rs publicly re-exports wasm::free, and pub fn free(ptr, len, align) calls std::alloc::dealloc after validating only the Layout. Safe wasm32 Rust code can therefore call craft_plugin_sdk::rt::free(0, 1, 1) or free an arbitrary/non-owned address, violating dealloc’s allocation/provenance contract and causing undefined behavior. A comment saying the host must supply a valid block does not impose an unsafe precondition on this public safe Rust API. Make the raw-pointer entry points unsafe where they rely on host ownership/lifetime assumptions (with explicit Safety contracts and correspondingly audited export glue), or validate ownership with an allocation registry so the safe surface is actually safe. Audit invoke/process for the same arbitrary-pointer-to-reference exposure rather than treating linear-memory bounds as proof of Rust allocation validity.
Separately, the requested SDK unsafe exception in RFC #414 question 4 is not yet named in this head’s AGENTS.md, which still requires workspace lint inheritance and prohibits unsafe absent a named exception. That policy decision must be resolved explicitly before this can be marked ready. Remaining SDK adapters/macro paths and current-head CI still need completion before approval.
Depends on #432. The branch sits on top of #431, #432 and #433, so the Files tab also shows their commits until they merge. Review two commits only:
craft-plugin-sdk: guest SDK and #[craft_plugin] macro for craft-abi 1 pluginsandcraft-plugin-sdk: format test manifests request fs.picked(two test fixtures). This PR uses none of the code in #431 and #433.Part of #414.
What
Two new crates. They are the guest side of
craft-plugin-abi. A plugin crate is acdylibthat depends oncraft-plugin-sdkand on nothing else.craft-plugin-sdk(crates/plugin-sdk) has these modules:host:call_json, the one function that talks tocraft_host.call, and typed helpers:log,info,cancelled,progress, and the modulescmd,state,doc,clock,randomandlink. A reply that does not fit the buffer is retried once with the size the host asks for.entry: request and response types for each contribution kind (command,hook,panel,format, and image bands foreffect,filter,object-filterandlive-effect).rt: the runtime behind the exports. Its safe parts are plain Rust over slices and run natively. The wasm32 part turns the raw pointers of the ABI into those slices and ownscraft_allocandcraft_free.backend: thecraft_host.callimport on wasm32. On other targets a test installs a fake host. With no fake host, a call returnsHostError::NoHost. It never returns a canned answer.testing(native only):with_host,reply,status. Handlers run in a plaincargo test.ui: a builder for panel widget trees.Widget::buildrejects duplicate ids and trees over 2,000 nodes.craft-plugin-sdk-macros(crates/plugin-sdk-macros) has one attribute,#[craft_plugin(manifest = "craft.toml")]. At compile time it reads the manifest, numbers the contributions, and checks the result withcraft_plugin_abi::manifest::Manifest::parse, the parser the install gate uses. A manifest the gate would reject is a compile error. It also checks that each contribution has a handler and that ids are unique. It emits:craft.manifestcustom section (wasm32 only),craft_abi,craft_alloc,craft_free, andcraft_invokeorcraft_processwhen a contribution needs them (wasm32 only),craft_testingmodule with the manifest and the dispatch tables, for native tests.Each crate has a README. The workflow
.github/workflows/plugin-sdk.ymlruns fmt, clippy, tests and the wasm32 build. The rootREADME.mdlists the crates.Size:
craft-plugin-sdk: 8 files undersrc/, 1299 lines. 47 tests in 6 files undertests/(1043 lines).craft-plugin-sdk-macros: 1 file undersrc/, 401 lines, 6 unit tests. 13 manifest fixtures (97 lines).Changes from our staging copy: comments that named an internal "contract" now point at the sections of
crates/plugin-abi/ABI.md, test names that said "contract" now say "abi" or "wire", and theuse super::*lines inhost.rsandrt.rsare explicit, because the workspace denieswildcard_imports. Two behavior changes:rt::wasmnow refuses a non-empty range at address 0. A Rust slice may not start at a null pointer. A host that sent a descriptor within_ptr = 0got a slice over address 0 before. It now gets status 3 (BAD_POINTER).tests/wasm_plugin.rsis new. See "Unsafe code" and "Checks".Why
Plugin authors write Rust. The SDK and the macro hide the ABI, so an author writes handler functions and a manifest. The compile step checks the manifest, so a bad manifest fails before the package is built. This is PR 6 of the series in #414. It depends on
craft-plugin-abi(#432) only, so it can merge before the plugin host.How apps and plugin authors use it
Apps do not depend on this crate. Plugin authors do. A plugin is a
craft.tomland an inline module of handler functions. This example was built forwasm32-unknown-unknownand run throughcraft_plugin_abi::check_packagefor this PR (187,170 bytes withopt-level = "s", no violations). Its native test also passes.craft.toml:src/lib.rs:Cargo.tomlhascrate-type = ["cdylib", "rlib"], the one dependencycraft-plugin-sdk, andunsafe_code = "deny". The linker must write a memory maximum, because the gate rejects a memory with none:-C link-arg=--max-memory=67108864forwasm32-unknown-unknown. The developer CLI sets this later. Until then it goes in.cargo/config.toml.Unsafe code
The workspace sets
unsafe_code = "forbid".craft-plugin-sdksetsdenyin its own manifest (it cannot inherit[lints] workspace = trueand override one lint, so it lists the other workspace lint levels itself). Guest glue needsunsafein two modules, each with an explicit#[allow(unsafe_code)]. RFC question 4 asks whether this exception is acceptable. Both modules exist on wasm32 only.Sites in the crates:
crates/plugin-sdk/src/backend.rs,unsafe extern "C"blockcraft_host.callimportbackend.rs, oneunsafe { craft_host_call(..) }crates/plugin-sdk/src/rt.rs,sliceslice::from_raw_parts&[u8]rt.rs,slice_mutslice::from_raw_parts_mut&mut [u8]ofcraft_process, checked not to overlap the input, params or descriptorrt.rs,allocstd::alloc::alloccraft_allocrt.rs,freestd::alloc::dealloccraft_freecrates/plugin-sdk-macros/src/lib.rs, generated#[unsafe(no_mangle)]oncraft_abi,craft_alloc,craft_free,craft_invoke,craft_process#[unsafe(link_section = "craft.manifest")]That is 4
unsafeblocks and 1unsafe externblock in the SDK, and 6 attributes in the macro output. Each block has aSAFETYcomment. The macro output carries#[allow(unsafe_code)]on the export module and the manifest static, so a plugin crate that setsdenycan use the macro. A crate that setsforbidcannot (E0453).tests/wasm_plugin.rsbuilds a fixture plugin for wasm32 and runs it inwasmi2.0.0 (a dev dependency) with a fakecraft_host.call. It is the only test that runs the unsafe code. It checkscraft_abi,craft_allocalignment and failure, a command that logs through the host import, a hook, a bad entry, a bad JSON request, a request pointer outside memory, an output that overlaps the input, an output at address 0 and an output past the end of memory. The fixture crate is written from the test file intoCARGO_TARGET_TMPDIR, so the repo holds no wasm file.Not in this PR
craft new,build,inspect,run,watch,dev,bindgen). It needs the plugin host crate and comes later.craft-plugin-ui(panel tree renderer).craft_state_saveandcraft_state_load. The macro does not export them yet.audio-effectkind. The macro refuses it with a compile error that says so.craft.sig).Dependencies
craft-plugin-abi(path, Add craft-plugin-abi: craft-abi 1 types, permission names and install gate #432).serdeandserde_jsonthrough[workspace.dependencies].craft-plugin-sdk-macros:proc-macro21.0.107,quote1.0.47,syn2.0.119 (allMIT OR Apache-2.0, already inCargo.lock) andtoml1.1.8 (MIT OR Apache-2.0, default features off, already inCargo.lock; its entry gains one dependency line,toml_writer, which was already locked).craft-plugin-sdkdev dependency:wasmi2.0.0 (MIT/Apache-2.0; featuresstd,validate,portable-dispatch, default features off). It bringswasmi_core,wasmi_ir,wasmi_collections(allMIT/Apache-2.0),spin0.9.9 (MIT),string-interner0.19.0 (MIT/Apache-2.0),hashbrown0.15.5 (MIT OR Apache-2.0),foldhash0.1.5 (Zlib) and a secondwasmparser0.228.0 (Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT). If maintainers do not want a dev engine in this PR, delete the four tests that use it. The other tests do not need it.cargo metadata. The repo has nodeny.toml, so no cargo-deny check exists.Cargo.lockgrows by 113 lines and drops 7. The 7 arewasmparserandfoldhashentries that now carry a version suffix, because a second version exists.Checks
Run on branch
pr/craft-plugin-sdk, from a clean worktree, withCARGO_TARGET_DIRoutside the repo.cargo fmt --check: no output, exit 0.cargo clippy -j4 --workspace --all-targets -- -D warnings: exit 0.cargo clippy -j4 -p craft-plugin-sdk -p craft-plugin-sdk-macros --all-targets --locked --target <T> -- -D warnings: exit 0 forx86_64-unknown-linux-gnu,x86_64-pc-windows-gnuandaarch64-apple-darwin.cargo clippy -j4 -p craft-plugin-sdk --locked --target wasm32-unknown-unknown -- -D warnings: exit 0 (the library; the integration tests usetesting, which is native only, and the macro crate is a proc macro, so neither builds for wasm32).cargo test -j4 -p craft-plugin-sdk -p craft-plugin-sdk-macros --locked: exit 0. SDK: 17host_calls, 9macro_plugin, 9rt, 4ui, 3format_plugin, 5wasm_plugin. Macros: 6. Total 53 passed, 0 failed, 2 ignored (the doc examples).cargo test -j4 --workspace --locked --no-fail-fastwithCARGO_PROFILE_DEV_DEBUG=0: exit 0. 2216 passed, 0 failed, 29 ignored.cargo build -j4 --target wasm32-unknown-unknown -p craft-plugin-sdk --locked: finished.RUSTDOCFLAGS="-D warnings" cargo doc -j4 -p craft-plugin-sdk -p craft-plugin-sdk-macros --no-deps --locked: finished.cargo test1 passed.cargo build --release --target wasm32-unknown-unknownfinished.check_packageon the result gave no violations and the manifestorg.example.greeterwith 2 contributions.Gated test:
tests/wasm_plugin.rsneeds thewasm32-unknown-unknowntarget and network access to the crates.io index on its first run (the fixture crate resolves its own dependencies, starting from the workspaceCargo.lock). When the target is missing, its five tests print "skipped" and pass. Every other test runs on a host with no extra setup.Not run: the new workflow on GitHub, the Windows and macOS test runs (clippy ran for both targets, tests did not), fuzzing of the manifest macro or of the descriptor handling, and any plugin on a real host engine of an app. The wasmi test engine in
wasm_plugin.rsis not the host that apps will use.