Skip to content

Add slice_to_array attribute for plain-Array slice imports#5145

Merged
guybedford merged 1 commit into
mainfrom
feat/slice-to-array
May 6, 2026
Merged

Add slice_to_array attribute for plain-Array slice imports#5145
guybedford merged 1 commit into
mainfrom
feat/slice-to-array

Conversation

@guybedford

@guybedford guybedford commented May 6, 2026

Copy link
Copy Markdown
Contributor

This adds the slice_to_array attribute for imported JS functions, an opt-in per-fn or per-extern "C"-block that lets idiomatic &[T] arguments be used when binding JS APIs that take a plain Array<T> rather than a typed array.

Idiomatic Rust uses &[T] for borrowed sequences. When binding a JS function that takes an array of values, the natural Rust signature is fn foo(items: &[T]). The default &[T] binding is split by element kind today: primitive T arrives as a typed-array view (Uint32Array etc.) because of a zero-copy buffer optimisation, while String and JS-imported types arrive as a plain Array. There's no way to ask for the plain-Array form for primitive T without abandoning &[T] for Vec<JsValue> or hand-rolled plumbing.

slice_to_array makes &[T] (and Option<&[T]>) arrive as a plain Array regardless of element kind, while keeping the user-facing &[T] signature unchanged:

#[wasm_bindgen]
extern "C" {
    // JS receives Array<number>, signature stays idiomatic.
    #[wasm_bindgen(slice_to_array)]
    fn set_indices(values: &[u16]);
}

Block-form for module bindings with a consistent convention:

#[wasm_bindgen(module = "/lib.js", slice_to_array)]
extern "C" {
    fn take_numbers(v: &[i32]);
    fn take_strings(v: &[String]);
    fn take_optional(v: Option<&[u16]>);
}

Implementation:

  • slice_to_array parsed in the macro at fn-arg, fn, and extern block levels; purely additive opt-in.
  • New internal VectorRefIntoWasmAbi trait dispatches the per-element ABI conversion. Two impl shapes, neither requiring T: Clone:
    • Primitive T: zero-copy borrow of the slice memory (no allocation, same wire as plain &[T]). The JS-side shim does Array.from(typedArrayView) and never frees the buffer.
    • Blanket for<'a> &'a T: Into<JsValue> (covers String, JsValue, and JS-imported types): iterate the slice and build a fresh [u32] index buffer, one externref per element. JS reads the indices into a plain Array and frees the index buffer.
  • New CLI instructions VectorLoadAsArray / OptionVectorLoadAsArray bind the resulting descriptor; the JS shim picks the right helper (and decides whether to free) based on the element VectorKind.
  • &[ExportedRustStruct] remains unsupported — the existing default &[T] doesn't support exported structs either, and a borrow form for them would require either deep cloning the data per element or reworking the exported-struct ownership model. Owned Vec<T> continues to work for that case.

Has no effect on exported functions; default &[T] (typed-array view / zero-copy memory borrow) and owned Vec<T> semantics are unchanged for callers that didn't opt in.

Test coverage:

  • wasm runtime tests in tests/wasm/slice_to_array.{rs,js} covering &[u8|u16|i32|f64], &[String], &[ImportedType], Option<&[u16]>, plus block-level fan-out.
  • reference test in crates/cli/tests/reference/slice-to-array.* locking in the bg.js / wat output, including the no-free codegen for primitive borrows.
  • new guide page at reference/attributes/on-js-imports/slice_to_array.html.

Comment thread crates/cli-support/src/js/binding.rs Outdated
Comment thread crates/cli/tests/reference/slice-to-array.bg.js

@hoodmane hoodmane left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally looks okay.

Adds an opt-in per-fn or per-`extern "C"`-block attribute that lets
idiomatic `&[T]` arguments be used when binding JS APIs that take a
plain `Array<T>` rather than a typed array.

Idiomatic Rust uses `&[T]` for borrowed sequences. When binding a JS
function that takes an array of values, the natural Rust signature is
`fn foo(items: &[T])`. The default `&[T]` binding is split by element
kind today: primitive `T` arrives as a typed-array view (`Uint32Array`
etc.) because of a zero-copy buffer optimisation, while `String` and
JS-imported types arrive as a plain `Array`. There's no way to ask
for the plain-`Array` form for primitive `T` without abandoning
`&[T]` for `Vec<JsValue>` or hand-rolled plumbing.

`slice_to_array` makes `&[T]` (and `Option<&[T]>`) arrive as a plain
`Array` regardless of element kind, while keeping the user-facing
`&[T]` signature unchanged:

    #[wasm_bindgen]
    extern "C" {
        // JS receives Array<number>, signature stays idiomatic.
        #[wasm_bindgen(slice_to_array)]
        fn set_indices(values: &[u16]);
    }

Block-form for module bindings with a consistent convention:

    #[wasm_bindgen(module = "/lib.js", slice_to_array)]
    extern "C" {
        fn take_numbers(v: &[i32]);
        fn take_strings(v: &[String]);
        fn take_optional(v: Option<&[u16]>);
    }

Implementation:

* `slice_to_array` parsed in the macro at fn-arg, fn, and extern block
  levels; purely additive opt-in.
* New internal `VectorRefIntoWasmAbi` trait dispatches the per-element
  ABI conversion. Two impl shapes, neither requiring `T: Clone`:
  - Primitive `T`: zero-copy borrow of the slice memory (no
    allocation, same wire as plain `&[T]`). The JS-side shim does
    `Array.from(typedArrayView)` and never frees the buffer.
  - Blanket `for<'a> &'a T: Into<JsValue>` (covers `String`,
    `JsValue`, and JS-imported types): iterate the slice and build a
    fresh `[u32]` index buffer, one externref per element. JS reads
    the indices into a plain `Array` and frees the index buffer.
* New CLI instructions `VectorLoadAsArray` /
  `OptionVectorLoadAsArray` bind the resulting descriptor; the JS
  shim picks the right helper (and decides whether to free) based on
  the element `VectorKind`.
* `&[ExportedRustStruct]` remains unsupported — the existing default
  `&[T]` doesn't support exported structs either, and a borrow form
  for them would require either deep cloning the data per element or
  reworking the exported-struct ownership model. Owned `Vec<T>`
  continues to work for that case.

Has no effect on exported functions; default `&[T]` (typed-array view
/ zero-copy memory borrow) and owned `Vec<T>` semantics are unchanged
for callers that didn't opt in.

Test coverage:

* wasm runtime tests in `tests/wasm/slice_to_array.{rs,js}` covering
  `&[u8|u16|i32|f64]`, `&[String]`, `&[ImportedType]`,
  `Option<&[u16]>`, plus block-level fan-out.
* reference test in `crates/cli/tests/reference/slice-to-array.*`
  locking in the bg.js / wat output, including the no-free codegen
  for primitive borrows.
* new guide page at
  `reference/attributes/on-js-imports/slice_to_array.html`.
@guybedford guybedford force-pushed the feat/slice-to-array branch from 4d6ef98 to d8d4ad2 Compare May 6, 2026 21:52
@guybedford guybedford merged commit 12646be into main May 6, 2026
65 checks passed
@guybedford guybedford deleted the feat/slice-to-array branch May 6, 2026 22:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants