تطوير الويب

WASI وخوادم WebAssembly: تنفيذ module بـ Wasmtime

3 دقائق للقراءة

📌 الدليل الرئيسي: WebAssembly في الإنتاج

تشغيل WebAssembly جانب الخادم يُغَيِّر القواعد مقارنة بالمتصفّح. ننتقل من بيئة wasm32-unknown-unknown، حيث كلّ شيء يمرّ عبر JavaScript، إلى هدف WASI حيث المكوّن يمكنه قراءة ملفّ، كشف خدمة HTTP، واستقبال قدرات صريحة من المضيف. Wasmtime، تصونه Bytecode Alliance، صار runtime مرجع نموذج المكوّنات: يدعم WASI 0.2 و0.3، يملك وضع AOT، يُضَمِّن debugger GDB، ويُوَزَّع كـ binary مستقلّ وcrate Rust.

يبني هذا الدليل مشروع WASI كامل: نُجَمِّع برنامج Rust إلى wasm32-wasip2، نُنَفِّذه عبر CLI Wasmtime، نُضيف endpoint HTTP عبر wasi-http، ثم نُضَمِّن المجموع في binary مضيف Rust يقود instantiation ويُقَيِّد القدرات.

الخطوة 1 — تثبيت Wasmtime وهدف Rust WASI

curl https://wasmtime.dev/install.sh -sSf | bash
exec $SHELL
wasmtime --version

المتوقّع 44.0.0 أو أحدث.

rustup target add wasm32-wasip2
rustup show

الهدف wasm32-wasip2 تيير 2 منذ Rust 1.82 ويُنتج مباشرة مكوّن WASI 0.2.

الخطوة 2 — أوّل برنامج WASI في Rust

cargo new --bin hello-wasi
cd hello-wasi
// src/main.rs
use std::env;

fn main() {
    let name = env::var("NAME").unwrap_or_else(|_| "anonyme".to_string());
    println!("Bonjour, {} ! Le binaire tourne dans Wasmtime.", name);
}
cargo build --target wasm32-wasip2 --release
ls target/wasm32-wasip2/release/hello-wasi.wasm

الـ binary يزن نمطيًّا 100-200 ko في release. file يُرجع « WebAssembly (wasm) binary module ».

الخطوة 3 — تنفيذ module عبر CLI Wasmtime

wasmtime target/wasm32-wasip2/release/hello-wasi.wasm

الخرج « Bonjour, anonyme! » — متغيّر NAME غير مُوَفَّر. نمنح الآن قدرة env صراحة:

wasmtime --env NAME=Mariama target/wasm32-wasip2/release/hello-wasi.wasm

هذه المرّة « Bonjour, Mariama! ». الـ --env KEY=VALUE قابل للتكرار. الفرق مع binary أصلي: كود WebAssembly لا يصل لأيّ متغيّر إلّا ما وَصَّله المضيف صراحة.

الخطوة 4 — منح وصول ملفّي محدود

// src/main.rs
use std::env;
use std::fs;

fn main() {
    let path = env::var("INPUT").unwrap_or_else(|_| "/data/message.txt".into());
    match fs::read_to_string(&path) {
        Ok(s)  => println!("Contenu : {}", s.trim()),
        Err(e) => eprintln!("Erreur lecture {} : {}", path, e),
    }
}
cargo build --target wasm32-wasip2 --release
mkdir -p data
echo "WebAssembly serveur" > data/message.txt
wasmtime --dir ./data::/data --env INPUT=/data/message.txt target/wasm32-wasip2/release/hello-wasi.wasm

الخرج « Contenu : WebAssembly serveur ». إن أزلت --dir، الاستدعاء يفشل بـ « No such file or directory » رغم وجود الملفّ: module لا يرى المسار حتى يُفتَح مسبقًا.

الخطوة 5 — كشف خدمة HTTP عبر wasi-http

WASI 0.2 يضمّ واجهة HTTP قياسية wasi:http/proxy. Wasmtime يعرف تنفيذها عبر wasmtime serve منذ Wasmtime 18.0.0.

cargo install cargo-component
cargo component new --lib http-service --proxy
cd http-service
use wasi::http::types::{Fields, IncomingRequest, OutgoingBody, OutgoingResponse, ResponseOutparam};
use wasi::exports::http::incoming_handler::Guest;

struct Component;

impl Guest for Component {
    fn handle(_req: IncomingRequest, out: ResponseOutparam) {
        let headers = Fields::new();
        headers.set(&"content-type".to_string(), &[b"application/json".to_vec()]).unwrap();
        let resp = OutgoingResponse::new(headers);
        resp.set_status_code(200).unwrap();
        let body = resp.body().unwrap();
        ResponseOutparam::set(out, Ok(resp));
        let stream = body.write().unwrap();
        stream.blocking_write_and_flush(br#"{"status":"ok","runtime":"wasmtime"}"#).unwrap();
        drop(stream);
        OutgoingBody::finish(body, None).unwrap();
    }
}

wasi::http::proxy::export!(Component);
cargo component build --release
wasmtime serve --addr 127.0.0.1:8080 target/wasm32-wasip2/release/http_service.wasm
curl -s http://127.0.0.1:8080/ping
# {"status":"ok","runtime":"wasmtime"}

خدمة HTTP كاملة، وظيفية، دون خادم Node أو Go: Wasmtime يتكلّم HTTP ويُمَرِّر الطلب للمكوّن.

الخطوة 6 — تضمين Wasmtime في مضيف Rust

cargo new --bin wasm-host
cd wasm-host
cargo add wasmtime@44 wasmtime-wasi@44 anyhow
use anyhow::Result;
use wasmtime::component::{Component, Linker, ResourceTable};
use wasmtime::{Config, Engine, Store};
use wasmtime_wasi::p2::{WasiCtx, WasiCtxBuilder, WasiCtxView, WasiView};

struct AppState { ctx: WasiCtx, table: ResourceTable }
impl WasiView for AppState {
    fn ctx(&mut self) -> WasiCtxView<'_> {
        WasiCtxView { ctx: &mut self.ctx, table: &mut self.table }
    }
}

fn main() -> Result<()> {
    let mut cfg = Config::new();
    cfg.wasm_component_model(true);
    let engine    = Engine::new(&cfg)?;
    let component = Component::from_file(&engine, "hello.wasm")?;
    let mut linker = Linker::::new(&engine);
    wasmtime_wasi::p2::add_to_linker_sync(&mut linker)?;
    let ctx       = WasiCtxBuilder::new().inherit_stdio().env("NAME", "hôte").build();
    let mut store = Store::new(&engine, AppState { ctx, table: ResourceTable::new() });
    let cmd       = wasmtime_wasi::p2::bindings::sync::Command::instantiate(&mut store, &component, &linker)?;
    cmd.wasi_cli_run().call_run(&mut store)?.map_err(|()| anyhow::anyhow!("module failed"))?;
    Ok(())
}

هذا الهيكل يُحَمِّل مكوّن CLI hello.wasm، يمنحه stdio المضيف ومتغيّر NAME، ثم يستدعي run. أيّ قدرة غير مُدرَجة (ملفّات، sockets، ساعة) مرفوضة افتراضيًّا. عكس std::process::Command الذي يَرِث كلّ شيء.

الخطوة 7 — وصف الواجهات الخاصّة بـ WIT

package itskc:demo;

interface fx {
    record amount    { value: f64, currency: string }
    record converted { usd:   f64, rate: f64 }
    convert: func(input: amount) -> result;
}

world fx-service {
    export fx;
}

في مكوّن Rust، cargo-component يُوَلِّد traits تلقائيًّا. في المضيف، يمكن استهلاك نفس ملفّ WIT لتحميل عدّة مكوّنات متزامنة بنفس الضمانات المكتَّبة. الـ binary المُنتج قابل للتركيب: نُسَلسِل مكوّنَين خرج أحدهما يُغَذِّي دخل الآخر.

الخطوة 8 — تقييد القدرات في الإنتاج

المبدأ التوجيهي: لا تمنح إلّا القدرات الضرورية بدقّة.

preopened_dir(path, guest_path, dir_perms, file_perms) يكشف مجلّدًا بصلاحيات منفصلة. يمكن السماح بالقراءة فقط على مجلّد إعدادات والكتابة على مجلّد scratch.

env(key, value) وargs(values) يكشفان فقط المتغيّرات والوسائط المختارة. المكوّنات لا تستطيع المرور على بيئة المضيف.

inherit_stdio() أو stdout(...)/stderr(...) يتحكّمان في وجهة كتابات المكوّن. يمكن إعادة توجيهها إلى buffer ذاكرة للتدقيق.

للشبكة، WASI 0.2 يُدخل wasi-sockets. Wasmtime يكشف allow_ip_name_lookup، socket_addr_check. رفض الكلّ افتراضيًّا ثم whitelist موقف الأمن الموصى به.

Wasmtime يكشف حدّين كمّيّين حرجَين في الإنتاج. حدّ الذاكرة عبر StoreLimitsBuilder::memory_size() يسقف linear memory. حدّ fuel عبر Store::set_fuel(n) يلتقط زمن CPU كتعليمات WebAssembly مُنَفَّذة؛ حين يتجاوز module، Wasmtime يرفع trap نظيفًا. مُجَمَّعَين، يُتيحان تنفيذ كود عدائي بضمانات صلبة لا يُوَفِّرها container Linux دون cgroups إضافية.

الخطوة 9 — أخطاء شائعة

العَرَض السبب الحلّ
error: unknown target « wasm32-wasip2 » Rust toolchain قبل 1.82 rustup update stable
Permission denied على القراءة preopen --dir غائب أضف --dir HOST::GUEST
wasmtime serve: unknown subcommand Wasmtime < 18.0.0 حدِّث إلى 44.x
مكوّن مرفوض من Wasmtime مُجَمَّع في wasm32-wasip1 بدل wasip2 أعد الترجمة بالهدف الصحيح
Crash traps: out of fuel حدّ fuel منخفض زد عبر Store::set_fuel أو عطِّل
زمن instantiation مرتفع لا precompile AOT wasmtime compile + cache قرصي

للتعمّق

مصادر رسمية

  • docs.wasmtime.dev
  • docs.wasmtime.dev/cli-options
  • component-model.bytecodealliance.org
  • github.com/bytecodealliance/cargo-component
  • github.com/WebAssembly/wasi-http
  • docs.wasmtime.dev/api/wasmtime
  • wasi.dev/interfaces

مع Wasmtime، نملك بيئة تنفيذ WebAssembly جانب الخادم كاملة: binaries صغيرة، instantiation سريع، قدرات صريحة. الخطوة التالية تَصنيع النشر — edge بـ Cloudflare Workers أو sandbox صارمة لكود مستخدم.

مقالات ذات صلة

Sponsoriser ce contenu

Cet emplacement est à vous

Position premium en fin d'article — c'est l'instant où les lecteurs sont le plus engagés. Réservez cet espace pour votre marque, votre formation ou votre offre.

Recevoir nos tarifs
Publicité