Trong bài viết WebAssembly là gì và từ đâu mà có?, Tôi đã giải thích cách chúng ta có được WebAssembly như ngày nay. Trong bài viết này, tôi sẽ trình bày phương pháp biên dịch một chương trình C hiện có, mkbitmap
, sang WebAssembly. Ví dụ này phức tạp hơn ví dụ hello world vì bao gồm việc xử lý tệp, giao tiếp giữa các vùng WebAssembly và JavaScript, cũng như vẽ vào canvas. Tuy nhiên, ví dụ này vẫn đủ dễ hiểu để bạn không cảm thấy quá tải.
Bài viết này dành cho những nhà phát triển web muốn tìm hiểu về WebAssembly và hướng dẫn từng bước cách bạn có thể tiến hành nếu muốn biên dịch một nội dung nào đó như mkbitmap
sang WebAssembly. Tôi xin cảnh báo trước rằng việc không biên dịch được một ứng dụng hoặc thư viện trong lần chạy đầu tiên là hoàn toàn bình thường. Đó là lý do khiến một số bước được mô tả bên dưới không hoạt động. Vì vậy, tôi cần phải quay lại và thử lại theo cách khác. Bài viết không cho thấy lệnh biên dịch cuối cùng như thể nó từ trên trời rơi xuống, mà mô tả tiến trình thực tế của tôi, bao gồm cả một số khó khăn.
Khoảng mkbitmap
Chương trình mkbitmap
C đọc một hình ảnh và áp dụng một hoặc nhiều thao tác sau theo thứ tự này: đảo ngược, lọc thông cao, chia tỷ lệ và ngưỡng. Bạn có thể kiểm soát và bật hoặc tắt từng thao tác riêng lẻ. Mục đích chính của mkbitmap
là chuyển đổi hình ảnh màu hoặc hình ảnh thang độ xám thành một định dạng phù hợp làm dữ liệu đầu vào cho các chương trình khác, đặc biệt là chương trình theo dõi potrace
tạo thành cơ sở của SVGcode. Là một công cụ tiền xử lý, mkbitmap
đặc biệt hữu ích trong việc chuyển đổi hình ảnh nét vẽ được quét (chẳng hạn như phim hoạt hình hoặc văn bản viết tay) thành hình ảnh hai cấp độ có độ phân giải cao.
Bạn sử dụng mkbitmap
bằng cách truyền cho nó một số lựa chọn và một hoặc nhiều tên tệp. Để biết tất cả thông tin chi tiết, hãy xem trang hướng dẫn sử dụng của công cụ này:
$ mkbitmap [options] [filename...]


mkbitmap -f 2 -s 2 -t 0.48
(Nguồn).Lấy mã
Bước đầu tiên là lấy mã nguồn của mkbitmap
. Bạn có thể tìm thấy thông tin này trên trang web của dự án. Tại thời điểm viết bài này, potrace-1.16.tar.gz là phiên bản mới nhất.
Biên dịch và cài đặt cục bộ
Bước tiếp theo là biên dịch và cài đặt công cụ này trên máy để cảm nhận cách công cụ hoạt động. Tệp INSTALL
chứa các chỉ dẫn sau:
cd
đến thư mục chứa mã nguồn và loại gói./configure
để định cấu hình gói cho hệ thống của bạn.Quá trình chạy
configure
có thể mất chút thời gian. Trong khi chạy, công cụ này sẽ in một số thông báo cho biết những tính năng mà công cụ đang kiểm tra.Nhập
make
để biên dịch gói.Bạn cũng có thể nhập
make check
để chạy mọi quy trình tự kiểm thử đi kèm với gói, thường là sử dụng các tệp nhị phân chưa cài đặt vừa được tạo.Nhập
make install
để cài đặt các chương trình, mọi tệp dữ liệu và tài liệu. Khi cài đặt vào một tiền tố thuộc quyền sở hữu của root, bạn nên định cấu hình và tạo gói dưới dạng người dùng thông thường, đồng thời chỉ thực thi giai đoạnmake install
bằng đặc quyền root.
Bằng cách làm theo các bước này, bạn sẽ có được hai tệp thực thi, potrace
và mkbitmap
. Tệp thứ hai là trọng tâm của bài viết này. Bạn có thể xác minh xem thao tác này có hoạt động đúng cách hay không bằng cách chạy mkbitmap --version
. Sau đây là kết quả của cả 4 bước trên máy của tôi, được cắt bớt đáng kể để ngắn gọn:
Bước 1, ./configure
:
$ ./configure
checking for a BSD-compatible install... /usr/bin/install -c
checking whether build environment is sane... yes
checking for a thread-safe mkdir -p... ./install-sh -c -d
checking for gawk... no
checking for mawk... no
checking for nawk... no
checking for awk... awk
checking whether make sets $(MAKE)... yes
[…]
config.status: executing libtool commands
Bước 2, make
:
$ make
/Applications/Xcode.app/Contents/Developer/usr/bin/make all-recursive
Making all in src
clang -DHAVE_CONFIG_H -I. -I.. -g -O2 -MT main.o -MD -MP -MF .deps/main.Tpo -c -o main.o main.c
mv -f .deps/main.Tpo .deps/main.Po
[…]
make[2]: Nothing to be done for `all-am'.
Bước 3, make check
:
$ make check
Making check in src
make[1]: Nothing to be done for `check'.
Making check in doc
make[1]: Nothing to be done for `check'.
[…]
============================================================================
Testsuite summary for potrace 1.16
============================================================================
# TOTAL: 8
# PASS: 8
# SKIP: 0
# XFAIL: 0
# FAIL: 0
# XPASS: 0
# ERROR: 0
============================================================================
make[1]: Nothing to be done for `check-am'.
Bước 4, sudo make install
:
$ sudo make install
Password:
Making install in src
.././install-sh -c -d '/usr/local/bin'
/bin/sh ../libtool --mode=install /usr/bin/install -c potrace mkbitmap '/usr/local/bin'
[…]
make[2]: Nothing to be done for `install-data-am'.
Để kiểm tra xem lệnh này có hoạt động hay không, hãy chạy mkbitmap --version
:
$ mkbitmap --version
mkbitmap 1.16. Copyright (C) 2001-2019 Peter Selinger.
Nếu nhận được thông tin chi tiết về phiên bản, tức là bạn đã biên dịch và cài đặt mkbitmap
thành công. Tiếp theo, hãy thực hiện các bước tương đương để hoạt động với WebAssembly.
Biên dịch mkbitmap
sang WebAssembly
Emscripten là một công cụ để biên dịch các chương trình C/C++ sang WebAssembly. Tài liệu Building Projects (Xây dựng dự án) của Emscripten nêu rõ như sau:
Việc tạo các dự án lớn bằng Emscripten rất dễ dàng. Emscripten cung cấp 2 tập lệnh đơn giản giúp định cấu hình các tệp makefile để sử dụng
emcc
làm giải pháp thay thế tức thì chogcc
. Trong hầu hết các trường hợp, phần còn lại của hệ thống xây dựng hiện tại trong dự án của bạn vẫn không thay đổi.
Sau đó, tài liệu tiếp tục (được chỉnh sửa một chút để ngắn gọn):
Hãy cân nhắc trường hợp bạn thường tạo bằng các lệnh sau:
./configure
make
Để tạo bằng Emscripten, thay vào đó, bạn sẽ dùng các lệnh sau:
emconfigure ./configure
emmake make
Về cơ bản, ./configure
trở thành emconfigure ./configure
và make
trở thành emmake make
. Sau đây là cách thực hiện việc này bằng mkbitmap
.
Bước 0, make clean
:
$ make clean
Making clean in src
rm -f potrace mkbitmap
test -z "" || rm -f
rm -rf .libs _libs
[…]
rm -f *.lo
Bước 1, emconfigure ./configure
:
$ emconfigure ./configure
configure: ./configure
checking for a BSD-compatible install... /usr/bin/install -c
checking whether build environment is sane... yes
checking for a thread-safe mkdir -p... ./install-sh -c -d
checking for gawk... no
checking for mawk... no
checking for nawk... no
checking for awk... awk
[…]
config.status: executing libtool commands
Bước 2, emmake make
:
$ emmake make
make: make
/Applications/Xcode.app/Contents/Developer/usr/bin/make all-recursive
Making all in src
/opt/homebrew/Cellar/emscripten/3.1.36/libexec/emcc -DHAVE_CONFIG_H -I. -I.. -g -O2 -MT main.o -MD -MP -MF .deps/main.Tpo -c -o main.o main.c
mv -f .deps/main.Tpo .deps/main.Po
[…]
make[2]: Nothing to be done for `all'.
Nếu mọi thứ diễn ra suôn sẻ, thì giờ đây sẽ có các tệp .wasm
ở đâu đó trong thư mục. Bạn có thể tìm thấy các tệp này bằng cách chạy find . -name "*.wasm"
:
$ find . -name "*.wasm"
./a.wasm
./src/mkbitmap.wasm
./src/potrace.wasm
Hai mục cuối cùng có vẻ hứa hẹn, vì vậy hãy cd
vào thư mục src/
. Hiện cũng có 2 tệp tương ứng mới là mkbitmap
và potrace
. Đối với bài viết này, chỉ có mkbitmap
là phù hợp. Việc các tệp này không có đuôi .js
có thể gây nhầm lẫn, nhưng thực tế đây là các tệp JavaScript, có thể xác minh bằng một lệnh gọi head
nhanh:
$ cd src/
$ head -n 20 mkbitmap
// include: shell.js
// The Module object: Our interface to the outside world. We import
// and export values on it. There are various ways Module can be used:
// 1. Not defined. We create it here
// 2. A function parameter, function(Module) { ..generated code.. }
// 3. pre-run appended it, var Module = {}; ..generated code..
// 4. External script tag defines var Module.
// We need to check if Module already exists (e.g. case 3 above).
// Substitution will be replaced with actual code on later stage of the build,
// this way Closure Compiler will not mangle it (e.g. case 4. above).
// Note that if you want to run closure, and also to use Module
// after the generated code, you will need to define var Module = {};
// before the code. Then that object will be used in the code, and you
// can continue to use Module afterwards as well.
var Module = typeof Module != 'undefined' ? Module : {};
// --pre-jses are emitted after the Module integration code, so that they can
// refer to Module (if they choose; they can also define Module)
Đổi tên tệp JavaScript thành mkbitmap.js
bằng cách gọi mv mkbitmap mkbitmap.js
(và mv potrace potrace.js
nếu bạn muốn).
Bây giờ, đã đến lúc thực hiện thử nghiệm đầu tiên để xem liệu có hiệu quả hay không bằng cách thực thi tệp bằng Node.js trên dòng lệnh bằng cách chạy node mkbitmap.js --version
:
$ node mkbitmap.js --version
mkbitmap 1.16. Copyright (C) 2001-2019 Peter Selinger.
Bạn đã biên dịch thành công mkbitmap
sang WebAssembly. Bây giờ, bước tiếp theo là làm cho ứng dụng hoạt động trong trình duyệt.
mkbitmap
bằng WebAssembly trong trình duyệt
Sao chép tệp mkbitmap.js
và mkbitmap.wasm
vào một thư mục mới có tên là mkbitmap
, đồng thời tạo một tệp index.html
HTML boilerplate tải tệp JavaScript mkbitmap.js
.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>mkbitmap</title>
</head>
<body>
<script src="mkbitmap.js"></script>
</body>
</html>
Khởi động một máy chủ cục bộ để phân phát thư mục mkbitmap
và mở thư mục đó trong trình duyệt. Bạn sẽ thấy một lời nhắc yêu cầu bạn nhập dữ liệu. Điều này là hoàn toàn bình thường vì theo trang hướng dẫn của công cụ này, "[i]f no filename arguments are given, then mkbitmap acts as a filter, reading from standard input" (nếu không có đối số tên tệp nào được đưa ra, thì mkbitmap sẽ hoạt động như một bộ lọc, đọc từ đầu vào tiêu chuẩn), theo mặc định, đây là prompt()
đối với Emscripten.
Ngăn chặn việc thực thi tự động
Để ngăn mkbitmap
thực thi ngay lập tức và thay vào đó là chờ người dùng nhập dữ liệu, bạn cần hiểu rõ đối tượng Module
của Emscripten. Module
là một đối tượng JavaScript chung có các thuộc tính mà mã do Emscripten tạo sẽ gọi tại nhiều thời điểm trong quá trình thực thi.
Bạn có thể cung cấp một phương thức triển khai Module
để kiểm soát việc thực thi mã.
Khi một ứng dụng Emscripten khởi động, ứng dụng sẽ xem xét các giá trị trên đối tượng Module
và áp dụng các giá trị đó.
Trong trường hợp mkbitmap
, hãy đặt Module.noInitialRun
thành true
để ngăn lần chạy ban đầu khiến lời nhắc xuất hiện. Tạo một tập lệnh có tên là script.js
, thêm tập lệnh đó trước <script src="mkbitmap.js"></script>
trong index.html
rồi thêm đoạn mã sau vào script.js
. Khi bạn tải lại ứng dụng, lời nhắc sẽ biến mất.
var Module = {
// Don't run main() at page load
noInitialRun: true,
};
Tạo bản dựng theo mô-đun với một số cờ bản dựng khác
Để cung cấp dữ liệu đầu vào cho ứng dụng, bạn có thể sử dụng tính năng hỗ trợ hệ thống tệp của Emscripten trong Module.FS
. Mục Bao gồm hỗ trợ hệ thống tệp trong tài liệu nêu rõ:
Emscripten tự động quyết định có bao gồm tính năng hỗ trợ hệ thống tệp hay không. Nhiều chương trình không cần đến tệp và hỗ trợ hệ thống tệp không có kích thước đáng kể, vì vậy Emscripten tránh đưa hệ thống tệp vào khi không thấy lý do cần thiết. Điều đó có nghĩa là nếu mã C/C++ của bạn không truy cập vào các tệp, thì đối tượng
FS
và các API hệ thống tệp khác sẽ không được đưa vào đầu ra. Mặt khác, nếu mã C/C++ của bạn có sử dụng các tệp, thì tính năng hỗ trợ hệ thống tệp sẽ tự động được đưa vào.
Rất tiếc, mkbitmap
là một trong những trường hợp mà Emscripten không tự động hỗ trợ hệ thống tệp, vì vậy, bạn cần phải cho Emscripten biết một cách rõ ràng để làm như vậy. Điều này có nghĩa là bạn cần làm theo các bước emconfigure
và emmake
như mô tả trước đó, với một vài cờ khác được đặt thông qua đối số CFLAGS
. Các cờ sau đây cũng có thể hữu ích cho các dự án khác.
- Đặt
-sFILESYSTEM=1
để có tính năng hỗ trợ hệ thống tệp. - Đặt
-sEXPORTED_RUNTIME_METHODS=FS,callMain
để xuấtModule.FS
vàModule.callMain
. - Đặt
-sMODULARIZE=1
và-sEXPORT_ES6
để tạo một mô-đun ES6 hiện đại. - Đặt
-sINVOKE_RUN=0
để ngăn lần chạy ban đầu khiến lời nhắc xuất hiện.
Ngoài ra, trong trường hợp cụ thể này, bạn cần đặt cờ --host
thành wasm32
để cho tập lệnh configure
biết rằng bạn đang biên dịch cho WebAssembly.
Lệnh emconfigure
cuối cùng sẽ có dạng như sau:
$ emconfigure ./configure --host=wasm32 CFLAGS='-sFILESYSTEM=1 -sEXPORTED_RUNTIME_METHODS=FS,callMain -sMODULARIZE=1 -sEXPORT_ES6 -sINVOKE_RUN=0'
Đừng quên chạy lại emmake make
và sao chép các tệp vừa tạo vào thư mục mkbitmap
.
Sửa đổi index.html
để chỉ tải mô-đun ES script.js
. Sau đó, bạn sẽ nhập mô-đun mkbitmap.js
từ mô-đun này.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>mkbitmap</title>
</head>
<body>
<!-- No longer load `mkbitmap.js` here -->
<script src="script.js" type="module"></script>
</body>
</html>
// This is `script.js`.
import loadWASM from './mkbitmap.js';
const run = async () => {
const Module = await loadWASM();
console.log(Module);
};
run();
Khi mở ứng dụng trong trình duyệt, bạn sẽ thấy đối tượng Module
được ghi vào nhật ký bảng điều khiển Công cụ cho nhà phát triển và lời nhắc đã biến mất, vì hàm main()
của mkbitmap
không còn được gọi khi bắt đầu.
Thực thi hàm chính theo cách thủ công
Bước tiếp theo là gọi thủ công hàm main()
của mkbitmap
bằng cách chạy Module.callMain()
. Hàm callMain()
lấy một mảng đối số, khớp từng đối số với những gì bạn sẽ truyền trên dòng lệnh. Nếu chạy mkbitmap -v
trên dòng lệnh, bạn sẽ gọi Module.callMain(['-v'])
trong trình duyệt. Thao tác này sẽ ghi số phiên bản mkbitmap
vào bảng điều khiển Công cụ cho nhà phát triển.
// This is `script.js`.
import loadWASM from './mkbitmap.js';
const run = async () => {
const Module = await loadWASM();
Module.callMain(['-v']);
};
run();
Chuyển hướng đầu ra chuẩn
Đầu ra chuẩn (stdout
) theo mặc định là bảng điều khiển. Tuy nhiên, bạn có thể chuyển hướng nó đến một nơi khác, chẳng hạn như một hàm lưu trữ đầu ra vào một biến. Điều này có nghĩa là bạn có thể thêm đầu ra vào HTML bằng cách đặt thuộc tính Module.print
.
// This is `script.js`.
import loadWASM from './mkbitmap.js';
const run = async () => {
let consoleOutput = 'Powered by ';
const Module = await loadWASM({
print: (text) => (consoleOutput += text),
});
Module.callMain(['-v']);
document.body.textContent = consoleOutput;
};
run();
Đưa tệp đầu vào vào hệ thống tệp bộ nhớ
Để đưa tệp đầu vào vào hệ thống tệp trong bộ nhớ, bạn cần có lệnh tương đương với mkbitmap filename
trên dòng lệnh. Để hiểu cách tôi tiếp cận vấn đề này, trước tiên, bạn cần biết một số thông tin cơ bản về cách mkbitmap
nhận dữ liệu đầu vào và tạo dữ liệu đầu ra.
Các định dạng đầu vào được hỗ trợ của mkbitmap
là PNM (PBM, PGM, PPM) và BMP. Định dạng đầu ra là PBM cho bitmap và PGM cho graymap. Nếu bạn cung cấp đối số filename
, theo mặc định, mkbitmap
sẽ tạo một tệp đầu ra có tên được lấy từ tên tệp đầu vào bằng cách thay đổi hậu tố của tệp đó thành .pbm
. Ví dụ: đối với tên tệp đầu vào example.bmp
, tên tệp đầu ra sẽ là example.pbm
.
Emscripten cung cấp một hệ thống tệp ảo mô phỏng hệ thống tệp cục bộ, nhờ đó mã gốc sử dụng các API tệp đồng bộ có thể được biên dịch và chạy mà không cần thay đổi hoặc chỉ cần thay đổi một chút.
Để mkbitmap
đọc một tệp đầu vào như thể tệp đó được truyền dưới dạng đối số dòng lệnh filename
, bạn cần sử dụng đối tượng FS
mà Emscripten cung cấp.
Đối tượng FS
được hỗ trợ bởi một hệ thống tệp trong bộ nhớ (thường được gọi là MEMFS) và có một hàm writeFile()
mà bạn dùng để ghi tệp vào hệ thống tệp ảo. Bạn sử dụng writeFile()
như trong đoạn mã mẫu sau.
Để xác minh rằng thao tác ghi tệp đã hoạt động, hãy chạy hàm readdir()
của đối tượng FS
bằng tham số '/'
. Bạn sẽ thấy example.bmp
và một số tệp mặc định luôn được tạo tự động.
Xin lưu ý rằng lệnh gọi trước đó đến Module.callMain(['-v'])
để in số phiên bản đã bị xoá. Điều này là do Module.callMain()
là một hàm thường chỉ chạy một lần.
// This is `script.js`.
import loadWASM from './mkbitmap.js';
const run = async () => {
const Module = await loadWASM();
const buffer = await fetch('https://example.com/example.bmp').then((res) => res.arrayBuffer());
Module.FS.writeFile('example.bmp', new Uint8Array(buffer));
console.log(Module.FS.readdir('/'));
};
run();
Lần thực thi thực tế đầu tiên
Khi mọi thứ đã sẵn sàng, hãy thực thi mkbitmap
bằng cách chạy Module.callMain(['example.bmp'])
. Ghi nhật ký nội dung của thư mục '/'
trong MEMFS và bạn sẽ thấy tệp đầu ra example.pbm
mới tạo bên cạnh tệp đầu vào example.bmp
.
// This is `script.js`.
import loadWASM from './mkbitmap.js';
const run = async () => {
const Module = await loadWASM();
const buffer = await fetch('https://example.com/example.bmp').then((res) => res.arrayBuffer());
Module.FS.writeFile('example.bmp', new Uint8Array(buffer));
Module.callMain(['example.bmp']);
console.log(Module.FS.readdir('/'));
};
run();
Lấy tệp đầu ra từ hệ thống tệp trong bộ nhớ
Hàm readFile()
của đối tượng FS
cho phép lấy example.pbm
được tạo ở bước cuối cùng ra khỏi hệ thống tệp trong bộ nhớ. Hàm này trả về một Uint8Array
mà bạn chuyển đổi thành đối tượng File
và lưu vào đĩa, vì các trình duyệt thường không hỗ trợ tệp PBM để xem trực tiếp trong trình duyệt.
(Có nhiều cách thanh lịch hơn để lưu tệp, nhưng cách sử dụng <a download>
được tạo động là cách được hỗ trợ rộng rãi nhất.) Sau khi lưu tệp, bạn có thể mở tệp đó trong trình xem ảnh mà bạn yêu thích.
// This is `script.js`.
import loadWASM from './mkbitmap.js';
const run = async () => {
const Module = await loadWASM();
const buffer = await fetch('https://example.com/example.bmp').then((res) => res.arrayBuffer());
Module.FS.writeFile('example.bmp', new Uint8Array(buffer));
Module.callMain(['example.bmp']);
const output = Module.FS.readFile('example.pbm', { encoding: 'binary' });
const file = new File([output], 'example.pbm', {
type: 'image/x-portable-bitmap',
});
const a = document.createElement('a');
a.href = URL.createObjectURL(file);
a.download = file.name;
a.click();
};
run();
Thêm giao diện người dùng tương tác
Đến thời điểm này, tệp đầu vào được mã hoá cứng và mkbitmap
chạy với các tham số mặc định. Bước cuối cùng là cho phép người dùng chọn tệp đầu vào một cách linh hoạt, điều chỉnh các tham số mkbitmap
, rồi chạy công cụ với các lựa chọn đã chọn.
// Corresponds to `mkbitmap -o output.pbm input.bmp -s 8 -3 -f 4 -t 0.45`.
Module.callMain(['-o', 'output.pbm', 'input.bmp', '-s', '8', '-3', '-f', '4', '-t', '0.45']);
Định dạng hình ảnh PBM không quá khó để phân tích cú pháp, vì vậy, với một số mã JavaScript, bạn thậm chí có thể cho thấy bản xem trước của hình ảnh đầu ra. Hãy xem mã nguồn của bản minh hoạ được nhúng bên dưới để biết một cách thực hiện việc này.
Kết luận
Xin chúc mừng! Bạn đã biên dịch thành công mkbitmap
sang WebAssembly và làm cho nó hoạt động trong trình duyệt! Có một số điểm dừng và bạn phải biên dịch công cụ nhiều lần cho đến khi công cụ hoạt động, nhưng như tôi đã viết ở trên, đó là một phần của trải nghiệm. Đừng quên thẻ webassembly
của StackOverflow nếu bạn gặp khó khăn. Chúc bạn biên dịch thành công!
Lời cảm ơn
Bài viết này được Sam Clegg và Rachel Andrew xem xét.