Rust-Wasm创建大尺寸Canvas ImageData缓冲区的栈溢出问题求解
我尝试在Rust中创建结构体存储HTML5 Canvas的ImageData(支持超过64×64像素),但画布尺寸超过100×100时JS抛出以下错误:
Uncaught RangeError: attempting to construct out-of-bounds Uint8ClampedArray on ArrayBuffer
loop DirectCanvas.tsx:23
DirectCanvas DirectCanvas.tsx:77
...
误以为是栈大小不足,尝试在config.toml中增加栈大小却报错:
= note: rust-lld: error: unknown argument: -Wl,-zstack-size=29491200
相关代码
Rust 结构体实现(src/lib.rs)
use wasm_bindgen::prelude::*; extern crate fixedbitset; extern crate web_sys; #[wasm_bindgen] pub struct CanvasSource { width: u32, height: u32, data: Vec<u8>, } #[wasm_bindgen] impl CanvasSource { pub fn width(&self) -> u32 { self.width } pub fn height(&self) -> u32 { self.height } // returns pointer to canvas image data pub fn data(&self) -> *const u8 { self.data.as_ptr() } pub fn cover_in_blood(&mut self) { let blood: Vec<u8> = vec![252, 3, 27, 255]; let mut new_data = blood.clone(); for _ in 0..self.width { for _ in 0..self.height { let pixel = blood.clone(); new_data = [new_data, pixel].concat() } } self.data = new_data; } pub fn new(width: u32, height: u32, initial_data: Vec<u8>) -> CanvasSource { let data_size = (width * height) as usize; let mut data = initial_data; data.resize(data_size, 0); CanvasSource { width, height, data, } } }
TypeScript 调用代码
import { useEffect, useRef, useState } from "react"; import { CanvasSource } from "rust-canvas-prototype"; import { memory } from "rust-canvas-prototype/rust_canvas_prototype_bg.wasm"; import styles from "./DirectCanvas.module.css"; const getRenderLoop = ( source: CanvasSource, ctx: CanvasRenderingContext2D, ) => { if (source && ctx) { const loop = () => { const sourceDataPtr = source.data(); const width = source.width(); const height = source.height(); const regionSize = width * height * 4; const pixelData = new Uint8ClampedArray( memory.buffer, sourceDataPtr, regionSize ) const imageData = new ImageData(pixelData, width, height); ctx.putImageData(imageData, 0, 0) }; return loop; } return null; } export function DirectCanvas() { const [source, setSource] = useState<CanvasSource>(); const [ctx, setCtx] = useState<CanvasRenderingContext2D | null>(null); const [paused, setPaused] = useState<boolean>(false); // undefined on init, null when paused const [animationId, setAnimationId] = useState<number>(0); const canvasElement = useRef<HTMLCanvasElement>(null); const initialized = source && ctx; // initialization useEffect(() => { if (!source) { console.log("loading source"); let [width, height] = [100, 100]; // uncomment below to cause error // [width, height] = [358, 358] setSource(CanvasSource.new(width, height, new Uint8Array([]))) } if (source && !ctx && canvasElement.current) { canvasElement.current.height = source.height(); canvasElement.current.width = source.width(); setCtx(canvasElement.current.getContext("2d")); } }, [source, ctx]) useEffect(() => { if (initialized) { const renderLoop = getRenderLoop(source, ctx); if (renderLoop) { renderLoop(); setTimeout(() => { setAnimationId(prev => prev + 1); }, 10) } } }, [source, ctx, animationId]); return ( <div className={styles.Container}> <span className={styles.Controls}> <button onClick={() => source?.cover_in_blood()}>Splatter</button> </span> <canvas ref={canvasElement}></canvas> </div> ) }
问题根源与修复方案
1. 核心错误:缓冲区大小计算错误
你的new方法中,data_size只计算了像素总数(width*height),但每个Canvas像素是RGBA4个字节,JS端读取时用了width * height *4作为长度,导致Rust侧缓冲区实际大小只有需要的1/4,JS访问时直接越界。
修复new方法:
pub fn new(width: u32, height: u32, initial_data: Vec<u8>) -> CanvasSource { // 计算总字节数:每个像素4个通道 let data_size = (width * height * 4) as usize; let mut data = initial_data; data.resize(data_size, 0); CanvasSource { width, height, data, } }
2. 次要错误:cover_in_blood方法性能极差且逻辑错误
原方法中用[new_data, pixel].concat()频繁复制数据,不仅性能极低,还会导致最终数据长度远超预期(初始new_data已有1个像素,循环又添加widthheight个,总长度是1 + widthheight*4字节),进一步加剧越界问题。
修复cover_in_blood方法:预分配足够空间,直接扩展数据:
pub fn cover_in_blood(&mut self) { let blood = [252, 3, 27, 255]; // 预分配刚好的空间:width*height个像素 ×4字节 let mut new_data = Vec::with_capacity((self.width * self.height * 4) as usize); for _ in 0..self.height { for _ in 0..self.width { new_data.extend_from_slice(&blood); } } self.data = new_data; }
3. 关于栈大小的误解
你的问题和栈大小完全无关:Vec<u8>是堆分配,栈上仅存储指针、长度和容量三个 usize 大小的数据,不会因画布尺寸增大而耗尽栈空间。
另外,WASM的LLD链接器不支持-Wl,-zstack-size参数,若确实需要调整WASM栈大小(比如递归过深等场景),应在Cargo.toml中使用正确的参数:
[profile.release] rustflags = ["-C", "link-arg=--stack-size=29491200"]
但此场景下完全不需要调整栈大小,修复上述两个逻辑错误即可解决问题。
内容的提问来源于stack exchange,提问作者ajazz

