You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Rust-Wasm创建大尺寸Canvas ImageData缓冲区的栈溢出问题求解

问题:Rust WASM Canvas缓冲区超出大小导致JS越界错误

我尝试在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.10 23:40:26