Phát triển WebAssembly VM viết bằng C
(irreducible.io)- Một lập trình viên đã dành các buổi tối và cuối tuần trong 6 tháng để xây dựng Semblance, một WebAssembly VM dựa trên C; thay vì một dự án phụ kết thúc nhanh, đây là dự án học tập dài hạn đào sâu vào đặc tả
- WebAssembly là bytecode tiêu chuẩn để chạy mã không đáng tin cậy trong trình duyệt với tốc độ gần native, và việc sử dụng ngoài trình duyệt cũng đang tăng lên, như Fastly, Shopify, Zed
- Mục tiêu của Semblance là tìm hiểu WebAssembly Core Specification và chuẩn bị để có thể đóng góp cho các runtime công nghiệp như Wasmtime, nên việc triển khai mọi opcode hay vượt qua core test suite không nằm trong phạm vi
- Interpreter hoạt động theo trình tự: kiểm tra magic bytes và phiên bản 1 của tệp
.wasm, giải mã các section, giải quyết import, khởi tạo instance của module, tra cứu hàm export, khởi tạo stack và activation frame, rồi chạy opcode switch loop - Đây không phải là runtime hoàn chỉnh nên vẫn chậm, có rò rỉ bộ nhớ và có thể còn lỗ hổng trước module độc hại, nhưng mục tiêu học đặc tả đã đạt được thông qua việc chạy Hello, World!
Vì sao chọn WebAssembly
- WebAssembly là tập lệnh tiêu chuẩn và định dạng bytecode dành cho máy stack ảo
- Điểm khởi đầu là trình biên dịch C/C++ sang JavaScript của Emscripten
- Emscripten chuyển LLVM IR thành JavaScript để chạy mã C và C++ trên web
- Các nhà phát triển trình duyệt và dự án Emscripten đã giới hạn mã sinh ra vào một tập con JavaScript đơn giản nhằm tối ưu hiệu năng, và tập con này được chuẩn hóa thành asm.js
- Sau đó, Wasm được thiết kế như một định dạng bytecode độc lập để tránh overhead khi parse JavaScript
- Gần đây, việc sử dụng WebAssembly ngoài trình duyệt cũng tăng lên
- Edge Compute của Fastly và Functions của Shopify được xây dựng trên engine WebAssembly
- Hệ thống extension của Zed cũng dựa trên Wasm
- Bytecode Alliance dẫn dắt nỗ lực toàn ngành nhằm hỗ trợ sử dụng WebAssembly ngoài trình duyệt, đồng thời phát triển runtime Wasmtime và chuẩn hóa WASI
- WASI cho phép module WebAssembly giao tiếp với hệ điều hành host thông qua các host call tương tự POSIX
- Cung cấp cách chuẩn để tương tác với các giao diện bên ngoài như standard input/output, filesystem, network
Từ dự án phụ thành dự án học tập
- Từng có một mô thức lặp lại: bắt đầu dự án phụ mới rồi mất hứng sau vài tuần và chuyển sang dự án khác
- Cần một dự án lớn có thể tập trung trong thời gian dài hơn, và xử lý các tầng thấp hơn của computing stack so với công việc thường ngày
- Vì đã có nhiều dự án phụ bị bỏ dở do thiếu định hướng, tác giả đang tìm một dự án có mục tiêu rõ ràng và kết quả nhìn thấy được
- Sau khi cảm thấy hồ sơ kỹ sư thiên về generalist ảnh hưởng đến việc tìm một công việc ổn định, tác giả tìm kiếm một lĩnh vực chuyên môn để xây dựng bộ kỹ năng hình chữ T
- Khả năng thiết kế môi trường tính toán tùy biến bằng custom system call trong WebAssembly trông hấp dẫn như một công cụ platform engineering
Mục tiêu và phạm vi của Semblance
- Tên dự án là Semblance, một interpreter WebAssembly viết bằng C
- Mục tiêu chính là làm quen với WebAssembly Core Specification
- Về dài hạn, mục tiêu cũng là tích lũy đủ kiến thức liên quan đến WebAssembly để có thể đóng góp cho các runtime công nghiệp như Wasmtime
- Vì là dự án phục vụ học tập, tác giả không có ý định triển khai mọi opcode hay vượt qua core test suite
- Có thể chạy “Hello, World!” được xem là tiêu chí thành công đủ tốt
- Tác giả thừa nhận mình không có kinh nghiệm phát triển C chuyên nghiệp, chất lượng mã C chưa lý tưởng, và hoan nghênh code review
Luồng chạy Hello, World!
- Mã C ví dụ khai báo hàm ngoài
putsvà in"Hello, World!\n"tronghello() - Tạo WebAssembly bằng Clang
--target=wasm32-nostdlib-Wl,--no-entry-Wl,--export-all-Wl,--allow-undefined-O3
- Tệp
hello.wasmđược chạy bằngsemblance hello.wasm --invoke hello - Khi chương trình bắt đầu, bộ nhớ cho các đối số dòng lệnh, module WebAssembly đã giải mã, và store chứa trạng thái runtime được khởi tạo trên stack
- Parse đối số dòng lệnh và thoát nếu không hợp lệ
Giải mã binary và cấu trúc module
- Interpreter trước tiên giải mã binary format của WebAssembly
- Kiểm tra magic bytes
"\0asm"ở đầu tệp - Xác minh phiên bản định dạng binary là
1 - Sau đó giải mã các sections có trong tệp
- Khi giải mã xong, struct
WasmModuleđược điền các function, type, import, data, v.v. củahello.wasmtypesfuncstablesmemsglobalselemsdatasstartimportsexportscustomsmeta
Giải quyết import và env::puts
- Host function mà Semblance cung cấp hiện chỉ có
env::puts - Import host function được xử lý bằng cách gán native function pointer vào store và đưa
funcaddrđược tạo vào mảngimportscủa module hostcall_putssử dụng i32 offset nhận được làm đối số- Lấy data pointer từ memory instance đầu tiên trong store
- In chuỗi ở vị trí offset ra standard output bằng
printf("%s", ...)
- Khi duyệt danh sách import, nếu tìm thấy mục có module name là
"env"và item name là"puts", host function được đăng ký bằngregister_hostcall_puts
Khởi tạo instance và thực thi opcode
- Sau khi giải quyết import, module được khởi tạo instance
- Quá trình khởi tạo instance bao gồm bước xác minh
- Thực hiện kiểm tra type và các test để xác nhận module có hình thức hợp lệ
- Khởi tạo memory, global, table
- Nếu có hàm
startthì gọi hàm đó
- Kết quả của việc khởi tạo instance là
WasmModuleInst, chứa cấu trúc thời điểm chạy và các export đã sẵn sàng sử dụng - Tìm hàm export dựa trên tên được chỉ định bằng đối số dòng lệnh
--invoke- Duyệt các export và trả về
WasmExportInstcó tên khớp - Xác nhận export được trả về là function
- Duyệt các export và trả về
- Việc gọi hàm cần khởi tạo stack và activation frame
- Phần thực thi cốt lõi là opcode switch loop
- Đọc lần lượt các Wasm instructions trong thân hàm
- Thực hiện thao tác trên stack và store tùy theo opcode
- Ví dụ xử lý
i32.const,i64.const,f32.const,f64.const,i32.ge_s,call,nop,unreachable,end, v.v. - Opcode chưa được xử lý sẽ in
"unhandled opcode [...]"và trả về trap
Lệnh call và gọi host function
- Xử lý
WasmOpCalldùngfuncidx, là immediate value của opcodecall, để tìm function instance trong store - Dựa trên thông tin function type, pop các đối số cần thiết khỏi stack
- Kiểm tra loại function instance để phân biệt đó là Wasm function hay native host function
- Nếu là host function, thực thi function pointer
hostfuncđã lưu bằng native call - Nếu có giá trị trả về thì push lại vào stack
- Khi hàm
hellochạm đến opcodeend, opcode switch loop kết thúc và quay lạimain
Kết quả và các hạn chế còn lại
- Kết quả chạy được hiển thị trên standard output như sau
Hello, World!Ok []
- Dự án có thể chạy một chương trình “Hello, World!” đơn giản
- Opcode coverage chưa hoàn chỉnh
- Mã còn lộn xộn, chậm và có rò rỉ bộ nhớ
- Có thể dễ bị tổn thương trước module độc hại
- Dù vậy, tác giả đã học được nhiều về WebAssembly Core Specification, và dự án giúp bước ra ngoài vùng quen thuộc với tư cách kỹ sư
- Sau khi tự xây dựng interpreter, tác giả cảm thấy mình đã có đủ kiến thức liên quan đến WebAssembly để có thể đóng góp cho các runtime công nghiệp như Wasmtime
- Trong tương lai có thể tiếp tục thêm vài opcode, nhưng tác giả muốn kết thúc giai đoạn xem dự án này là dự án chính
1 bình luận
Các ý kiến trên Hacker News
Trước đây tôi từng làm một trình thông dịch Wasm bằng Scheme, nên rất vui khi thấy ngày càng có thêm người tự triển khai
Việc này không khó như tưởng tượng, nên bạn có thể đọc thử đặc tả rồi bắt tay làm; cũng không cần triển khai mọi lệnh, chỉ cần làm đến mức thấy thú vị là đủ
Với tư cách một người cũng tự triển khai, tôi có một mẹo: trong spec-test có nhiều định dạng Wasm dạng văn bản kỳ lạ mà cách biên dịch không trực quan, nhưng nếu dùng bộ chuyển đổi
wast2jsonthì có thể tạo ra phần mô tả JSON đơn giản hơn cùng với các tệp Wasm nhị phân thông thườngLàm cho bộ kiểm thử này chạy nhanh sẽ giúp tăng tốc độ lặp và độ chính xác
Phải mất một thời gian để mọi thứ hoạt động, nhưng một khi đã thành công thì chúng tôi tiến rất nhanh tới điểm có thể chạy bất cứ thứ gì; dù bộ kiểm thử không hoàn chỉnh, nó vẫn bao phủ khoảng 95%: https://github.com/WebAssembly/testsuite
Trước đây tôi từng thấy các bài kiểm thử này, nhưng không biết dùng thế nào nên khá bực và rối
Liên quan đến việc diễn giải Wasm trực tiếp, bài báo này khá thú vị: https://arxiv.org/abs/2205.01183
Dựa trên đó tôi đã tạo https://github.com/peterseymour/winter, và học được rằng Wasm không đơn giản như mình nghĩ
Câu hỏi của người mới: tôi tò mò việc debug trình thông dịch được thực hiện thế nào trong tình huống bạn không viết mã trực tiếp nhắm tới nó
Tôi cũng muốn biết cách fuzz chuỗi opcode hữu ích đến mức nào, và khác biệt thực tế giữa engine Wasm phía server với engine dựa trên trình duyệt lớn ra sao, cũng như cần bao nhiêu công sức để đổi từ cái này sang cái kia
Cách tiếp cận thú vị và công việc rất tốt
Ai muốn xem phần cốt lõi thì phần lớn nằm trong tệp này: https://github.com/irrio/semblance/blob/main/src/wrun.c
Nghĩ lại thì có lẽ sẽ tốt hơn nếu dự án đi theo giao diện tiêu chuẩn Wasm-C-API(https://github.com/WebAssembly/wasm-c-api)
Nó vốn đã là C API và được hầu hết runtime Wasm như Wasmer, V8, wasmi, v.v. chấp nhận, nên các nhà phát triển quen với API đó có thể thử nghiệm dễ dàng hơn
Nếu tác giả đủ quen thuộc với Wasm và muốn đóng góp cho Wasmer thì các bản vá hoặc cải tiến cũng rất được hoan nghênh
Họ nói “những lo ngại có thể hiểu được về việc Wasmer, một công ty được VC đầu tư, đã cố đăng ký WebAssembly — tên của một tổ chức phi lợi nhuận — làm nhãn hiệu”, và đó là sự thừa nhận sai lầm
Installed-Size: 266 MBư, cái quái gì vậyhttps://github.com/wasmerio/wasmer/issues/2615 bị bỏ mặc rồi tự động đóng
Một câu hỏi gây tranh luận hơn một chút: tôi tò mò liệu có ý định thêm lệnh tail call dự phòng không
Phía đặc tả Wasm đã từ chối với lý do nó quá “cao cấp”, nhưng ủy ban C cũng từng bác đề xuất của Dennis Ritchie
Dù vậy tôi vẫn đặt tiền vào phía Ritchie, và có vẻ Rob Pike cũng đã đặt cược theo hướng đó
Nếu không thì tại sao lại tạo ra Golang? Chỉ khi call là bậc cao thì tail call mới là bậc cao
Bạn nên xem thử Orca. Có vẻ bạn có thể đóng góp tốt cho dự án đó
[0] https://orca-app.dev
Tôi cũng đã quyết định tập trung vào một dự án thay vì nhảy qua lại giữa những thứ mới mẻ hào nhoáng, nhưng ngoại lệ là tôi đang giao việc vặt cho các trợ lý AI
Nói nhẹ nhất thì cũng khá bực bội
Và tôi cũng định đề xuất dùng một thư viện như
libffiđể xử lý nhiều đối số, v.v. khi gọi từ CKhi dùng WebAssembly làm plugin API ở những nơi như zed, tôi tò mò các lập trình viên plugin debug mã của họ thế nào
Ví dụ, có thể debug bằng breakpoint không, và nếu mã crash thì có nhận được stack trace không