iT邦幫忙

2025 iThome 鐵人賽

DAY 12
0
Rust

把前端加速到天花板:Rust+WASM 即插即用外掛系列 第 12

Day 11|Result 的錯誤物語:達達的馬蹄與 {code, message}

  • 分享至 

  • xImage
  •  

到目前為止,我們把重心放在一些酷酷的地方(寫了很多功能與演算法)但錯誤可觀測性也重要。WASM 這一端如果只是 return Err(JsValue::from_str("something bad")),前端拿到的就會是一條字串,既不利於分流處置,也不便於觀測串接。今天把錯誤合成一個漂亮的介面:Rust 端回傳 Result<T, JsValue>,至少帶兩個欄位:{ code, message }code 是穩定機器碼,message 是給人看的說明,方便在 UI、Log、監控上做對應處理。

我在 Rust 內部定義一個 ErrorCode 列舉與輕量結構 JsError { code, message };所有導出的 #[wasm_bindgen] 函式都只回傳 Result<..., JsValue>,失敗時透過 serde_wasm_bindgen::to_value(&JsError{...}) 序列化成真正的 JS 物件。這樣前端 catch 到的就是 { code, message },可以像一般 API 失敗一樣判斷、顯示、上報。

Rust 的部分

把 Err 都變成 {code, message} 物件

use wasm_bindgen::prelude::*;
use serde::{Deserialize, Serialize};
use serde_wasm_bindgen as swb;

// 1) 定義錯誤碼
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
enum ErrorCode {
    InputLengthMismatch,
    BadOps,
    Unsupported,
    Internal,
}

// 2) 傳給 JS 的錯誤物件
#[derive(Clone, Debug, Serialize, Deserialize)]
struct JsError<'a> {
    code: ErrorCode,
    message: &'a str,
}

// 3) 把 (code, message) 轉成 JsValue
fn js_err(code: ErrorCode, message: &str) -> JsValue {
    swb::to_value(&JsError { code, message }).unwrap_or_else(|_| {
        JsValue::from_str(message)
    })
}

接著,把公開 API 的 Err 都換成這個格式。

#[wasm_bindgen]
pub fn apply_pipeline(input: &[u8], w: u32, h: u32, ops: &JsValue) -> Result<Vec<u8>, JsValue> {
    let expected = (w as usize) * (h as usize) * 4;
    if input.len() != expected {
        return Err(js_err(ErrorCode::InputLengthMismatch, "input length mismatch"));
    }

    let ops: Vec<Op> = swb::from_value(ops.clone())
        .map_err(|e| js_err(ErrorCode::BadOps, &format!("bad ops: {e}")))?;

    // ...照舊處理,遇到不支援的分支可以回報:
    // return Err(js_err(ErrorCode::Unsupported, "unsupported op: ..."));

    let mut buf = input.to_vec();
    for op in ops {
        buf = match op {
            Op::Grayscale => grayscale(&buf, w, h),
            Op::BrightnessContrast { b, c } => brightness_contrast(&buf, w, h, b, c),
            Op::Blur { r } => box_blur_rgba(&buf, w, h, r),
            Op::Conv3x3 { k } => convolve3x3(&buf, w, h, &k),
            Op::Unsharp { r, amount, threshold, limit } =>
                unsharp(&buf, w, h, r, amount, threshold, limit),
            Op::EdgeSobel => edge_sobel(&buf, w, h),
        };
    }
    Ok(buf)
}

#[wasm_bindgen]
pub fn apply_pipeline_fast(input: &[u8], w: u32, h: u32, ops: &JsValue) -> Result<Vec<u8>, JsValue> {
    let expected = (w as usize) * (h as usize) * 4;
    if input.len() != expected {
        return Err(js_err(ErrorCode::InputLengthMismatch, "input length mismatch"));
    }
    let ops: Vec<Op> = swb::from_value(ops.clone())
        .map_err(|e| js_err(ErrorCode::BadOps, &format!("bad ops: {e}")))?;

    let mut a = input.to_vec();
    let mut b = vec![0u8; expected];
    let mut toggle = false;

    for op in ops {
        match op {
            Op::Grayscale => { if !toggle { grayscale_into(&a, &mut b); } else { grayscale_into(&b, &mut a); } }
            Op::BrightnessContrast { b: br, c } => {
                if !toggle { brightness_contrast_into(&a, &mut b, br, c); }
                else       { brightness_contrast_into(&b, &mut a, br, c); }
            }
            Op::Blur { r } => {
                if !toggle { box_blur_rgba_into(&a, &mut b, w, h, r); }
                else       { box_blur_rgba_into(&b, &mut a, w, h, r); }
            }
            Op::Conv3x3 { k } => {
                if !toggle { convolve3x3_into(&a, &mut b, w, h, &k); }
                else       { convolve3x3_into(&b, &mut a, w, h, &k); }
            }
            Op::Unsharp { r, amount, threshold, limit } => {
                if !toggle { unsharp_into(&a, &mut b, w, h, r, amount, threshold, limit); }
                else       { unsharp_into(&b, &mut a, w, h, r, amount, threshold, limit); }
            }
            Op::EdgeSobel => {
                if !toggle { edge_sobel_into(&a, &mut b, w, h); }
                else       { edge_sobel_into(&b, &mut a, w, h); }
            }
        }
        toggle = !toggle;
    }

    Ok(if toggle { b } else { a })
}

#[wasm_bindgen]
pub fn apply_pipeline_into(
    input: &[u8],
    w: u32,
    h: u32,
    ops: &JsValue,
    out: &mut [u8],
) -> Result<(), JsValue> {
    let expected = (w as usize) * (h as usize) * 4;
    if input.len() != expected || out.len() != expected {
        return Err(js_err(ErrorCode::InputLengthMismatch, "input or output length mismatch"));
    }
    let ops: Vec<Op> = swb::from_value(ops.clone())
        .map_err(|e| js_err(ErrorCode::BadOps, &format!("bad ops: {e}")))?;

    // 這裡示範最簡單的「A=輸入, B=out」乒乓
    let mut a = input.to_vec();
    let b_ptr = out as *mut [u8] as *mut u8; // 只是為了清楚指出 out 是目的端
    let mut toggle = false;

    for op in ops {
        match op {
            Op::Grayscale => { if !toggle { grayscale_into(&a, out); } else { grayscale_into(out, &mut a); } }
            Op::BrightnessContrast { b: br, c } => {
                if !toggle { brightness_contrast_into(&a, out, br, c); }
                else       { brightness_contrast_into(out, &mut a, br, c); }
            }
            Op::Blur { r } => {
                if !toggle { box_blur_rgba_into(&a, out, w, h, r); }
                else       { box_blur_rgba_into(out, &mut a, w, h, r); }
            }
            Op::Conv3x3 { k } => {
                if !toggle { convolve3x3_into(&a, out, w, h, &k); }
                else       { convolve3x3_into(out, &mut a, w, h, &k); }
            }
            Op::Unsharp { r, amount, threshold, limit } => {
                if !toggle { unsharp_into(&a, out, w, h, r, amount, threshold, limit); }
                else       { unsharp_into(out, &mut a, w, h, r, amount, threshold, limit); }
            }
            Op::EdgeSobel => {
                if !toggle { edge_sobel_into(&a, out, w, h); }
                else       { edge_sobel_into(out, &mut a, w, h); }
            }
        }
        toggle = !toggle;
    }

    // 如果最後結果在 a,就複回 out(確保呼叫者拿到的是最新)
    if !toggle {
        out.copy_from_slice(&a);
    }
    Ok(())
}

你可以看到,除了把 Err(...) 的生成改成 js_err(...),其餘的完全沒變。

前端部分

前端這邊,我建議做一個簡單的 type guard,統一處理三種情形:

  1. WASM 依規格拋回 { code, message }
  2. 第三方或舊版還在拋字串;
  3. 真的拋了個不明東西。
type WasmError = { code: string; message: string }

function isWasmError(e: unknown): e is WasmError {
  return !!e && typeof e === 'object'
      && 'code' in (e as any) && 'message' in (e as any)
      && typeof (e as any).code === 'string'
      && typeof (e as any).message === 'string'
}

function showError(e: unknown) {
  if (isWasmError(e)) {
    // 機器碼可用於分流/上報;訊息給人看
    console.error(`[WASM] ${e.code}: ${e.message}`)
    alert(`${e.code}\n${e.message}`)
  } else if (typeof e === 'string') {
    console.error('[WASM]', e)
    alert(e)
  } else {
    console.error('[WASM] Unknown error', e)
    alert('Unknown error')
  }
}

runPipelinerunPipelineInto、或任何呼叫包成 try/catch,錯誤直接丟給 showError

const runPipeline = (ops: unknown[], useFast = true) => {
  if (!w || !h) return
  const imgData = ctx.getImageData(0, 0, w, h)
  const input = new Uint8Array(imgData.data.buffer)
  try {
    const pipe = useFast ? fastPipe : slowPipe
    const out = pipe(input, w, h, ops) as Uint8Array
    imgData.data.set(out)
    ctx.putImageData(imgData, 0, 0)
  } catch (e) {
    showError(e)
  }
}

這樣 UI 能根據 code 做分支:例如 InputLengthMismatch 直接提示「圖片尺寸或像素長度不符」;BadOps 顯示「參數格式錯誤」;Unsupported 可以偵錯 OPS 內容;Internal 則走一般的錯誤回報流程。

https://ithelp.ithome.com.tw/upload/images/20250926/20162491BgypAXZu57.png


上一篇
Day 10|Sobel 邊緣偵測:抓到你了,臭邊緣!
下一篇
Day 13|請你們家勞公出來喔~(Web Worker
系列文
把前端加速到天花板:Rust+WASM 即插即用外掛18
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言