Làm cách nào để tích hợp WebAssembly vào chế độ thiết lập này? Trong bài viết này, chúng ta sẽ tìm hiểu vấn đề này bằng C/C++ và Emscripten làm ví dụ.
WebAssembly (wasm) thường được coi là một nguyên tắc cơ bản về hiệu suất hoặc một cách để chạy cơ sở mã C++ hiện có trên web. Với squoosh.app, chúng tôi muốn cho thấy rằng có ít nhất một góc nhìn thứ ba về wasm: tận dụng hệ sinh thái rộng lớn của các ngôn ngữ lập trình khác. Với Emscripten, bạn có thể sử dụng mã C/C++, Rust có hỗ trợ wasm được tích hợp sẵn và nhóm Go cũng đang nỗ lực thực hiện việc này. Tôi chắc chắn rằng nhiều ngôn ngữ khác sẽ được hỗ trợ sau này.
Trong những trường hợp này, wasm không phải là tâm điểm của ứng dụng mà là một mảnh ghép: một mô-đun khác. Ứng dụng của bạn đã có JavaScript, CSS, thành phần hình ảnh, hệ thống bản dựng tập trung vào web và thậm chí có thể có một khung như React. Làm cách nào để tích hợp WebAssembly vào chế độ thiết lập này? Trong bài viết này, chúng ta sẽ tìm hiểu vấn đề này bằng C/C++ và Emscripten làm ví dụ.
Docker
Tôi thấy Docker rất hữu ích khi làm việc với Emscripten. Các thư viện C/C++ thường được viết để hoạt động với hệ điều hành mà chúng được xây dựng. Việc có một môi trường nhất quán là vô cùng hữu ích. Với Docker, bạn sẽ có một hệ thống Linux ảo đã được thiết lập để hoạt động với Emscripten và đã cài đặt tất cả các công cụ cũng như phần phụ thuộc. Nếu thiếu một thành phần nào đó, bạn chỉ cần cài đặt thành phần đó mà không cần lo lắng về việc thành phần đó ảnh hưởng đến máy của bạn hoặc các dự án khác của bạn. Nếu có vấn đề xảy ra, hãy vứt bỏ hộp đựng và bắt đầu lại. Nếu hoạt động một lần, bạn có thể chắc chắn rằng hoạt động đó sẽ tiếp tục và tạo ra kết quả giống hệt nhau.
Docker Registry có một hình ảnh Emscripten của trzeci mà tôi đã sử dụng rộng rãi.
Tích hợp với npm
Trong hầu hết các trường hợp, điểm truy cập vào một dự án web là package.json
của npm. Theo quy ước, hầu hết các dự án đều có thể được tạo bằng npm install &&
npm run build
.
Nhìn chung, các cấu phần phần mềm bản dựng do Emscripten tạo ra (tệp .js
và .wasm
) chỉ nên được coi là một mô-đun JavaScript khác và một tài sản khác. Tệp JavaScript có thể được xử lý bằng một trình đóng gói như webpack hoặc rollup và tệp wasm phải được coi như bất kỳ tài sản nhị phân lớn nào khác, chẳng hạn như hình ảnh.
Do đó, bạn cần tạo các cấu phần phần mềm bản dựng Emscripten trước khi quy trình tạo bản dựng "bình thường" bắt đầu:
{
"name": "my-worldchanging-project",
"scripts": {
"build:emscripten": "docker run --rm -v $(pwd):/src trzeci/emscripten
./build.sh",
"build:app": "<the old build command>",
"build": "npm run build:emscripten && npm run build:app",
// ...
},
// ...
}
Tác vụ build:emscripten
mới có thể trực tiếp gọi Emscripten, nhưng như đã đề cập trước đó, bạn nên sử dụng Docker để đảm bảo môi trường xây dựng nhất quán.
docker run ... trzeci/emscripten ./build.sh
yêu cầu Docker khởi động một vùng chứa mới bằng cách dùng hình ảnh trzeci/emscripten
và chạy lệnh ./build.sh
.
build.sh
là một tập lệnh shell mà bạn sẽ viết tiếp theo! --rm
yêu cầu Docker xoá vùng chứa sau khi vùng chứa chạy xong. Bằng cách này, bạn sẽ không tích luỹ một bộ sưu tập các hình ảnh máy lỗi thời theo thời gian. -v $(pwd):/src
có nghĩa là bạn muốn Docker "phản chiếu" thư mục hiện tại ($(pwd)
) sang /src
bên trong vùng chứa. Mọi thay đổi bạn thực hiện đối với các tệp trong thư mục /src
bên trong vùng chứa sẽ được phản ánh vào dự án thực tế của bạn. Các thư mục được sao chép này được gọi là "thư mục liên kết".
Hãy cùng xem build.sh
:
#!/bin/bash
set -e
export OPTIMIZE="-Os"
export LDFLAGS="${OPTIMIZE}"
export CFLAGS="${OPTIMIZE}"
export CXXFLAGS="${OPTIMIZE}"
echo "============================================="
echo "Compiling wasm bindings"
echo "============================================="
(
# Compile C/C++ code
emcc \
${OPTIMIZE} \
--bind \
-s STRICT=1 \
-s ALLOW_MEMORY_GROWTH=1 \
-s MALLOC=emmalloc \
-s MODULARIZE=1 \
-s EXPORT_ES6=1 \
-o ./my-module.js \
src/my-module.cpp
# Create output folder
mkdir -p dist
# Move artifacts
mv my-module.{js,wasm} dist
)
echo "============================================="
echo "Compiling wasm bindings done"
echo "============================================="
Có rất nhiều điều cần phân tích ở đây!
set -e
sẽ chuyển shell sang chế độ "thất bại nhanh". Nếu bất kỳ lệnh nào trong tập lệnh trả về lỗi, toàn bộ tập lệnh sẽ bị huỷ ngay lập tức. Điều này có thể vô cùng hữu ích vì đầu ra cuối cùng của tập lệnh sẽ luôn là thông báo thành công hoặc lỗi khiến bản dựng không thành công.
Với các câu lệnh export
, bạn xác định giá trị của một số biến môi trường. Các biến này cho phép bạn truyền các tham số dòng lệnh bổ sung đến trình biên dịch C (CFLAGS
), trình biên dịch C++ (CXXFLAGS
) và trình liên kết (LDFLAGS
). Tất cả các biến này đều nhận được chế độ cài đặt trình tối ưu hoá thông qua OPTIMIZE
để đảm bảo mọi thứ đều được tối ưu hoá theo cùng một cách. Có một số giá trị có thể có cho biến OPTIMIZE
:
-O0
: Không tối ưu hoá. Không có mã không dùng đến nào bị loại bỏ và Emscripten cũng không giảm thiểu mã JavaScript mà nó phát ra. Phù hợp để gỡ lỗi.-O3
: Tối ưu hoá mạnh mẽ để cải thiện hiệu suất.-Os
: Tối ưu hoá mạnh mẽ về hiệu suất và kích thước như một tiêu chí phụ.-Oz
: Tối ưu hoá mạnh mẽ về kích thước, hy sinh hiệu suất nếu cần.
Đối với web, tôi chủ yếu đề xuất -Os
.
Lệnh emcc
có vô số tuỳ chọn riêng. Xin lưu ý rằng emcc được cho là "một giải pháp thay thế tức thì cho các trình biên dịch như GCC hoặc clang". Vì vậy, tất cả các cờ mà bạn có thể biết từ GCC rất có thể sẽ được emcc triển khai. Cờ -s
đặc biệt ở chỗ nó cho phép chúng ta định cấu hình Emscripten một cách cụ thể. Bạn có thể tìm thấy tất cả các lựa chọn có sẵn trong settings.js
của Emscripten, nhưng tệp đó có thể khá lớn. Sau đây là danh sách các cờ Emscripten mà tôi cho là quan trọng nhất đối với nhà phát triển web:
--bind
cho phép embind.-s STRICT=1
ngừng hỗ trợ tất cả các lựa chọn bản dựng không dùng nữa. Điều này đảm bảo rằng mã của bạn được tạo theo cách tương thích về sau.-s ALLOW_MEMORY_GROWTH=1
cho phép bộ nhớ tự động tăng lên nếu cần. Tại thời điểm viết, Emscripten sẽ phân bổ ban đầu 16 MB bộ nhớ. Khi mã của bạn phân bổ các khối bộ nhớ, lựa chọn này sẽ quyết định xem các thao tác này có khiến toàn bộ mô-đun wasm gặp lỗi khi hết bộ nhớ hay không, hoặc liệu mã kết dính có được phép mở rộng tổng bộ nhớ để đáp ứng việc phân bổ hay không.-s MALLOC=...
chọn phương thức triển khaimalloc()
nào sẽ sử dụng.emmalloc
là một phương thức triển khaimalloc()
nhỏ và nhanh chóng dành riêng cho Emscripten. Giải pháp thay thế làdlmalloc
, một phương thức triển khaimalloc()
hoàn chỉnh. Bạn chỉ cần chuyển sangdlmalloc
nếu thường xuyên phân bổ nhiều đối tượng nhỏ hoặc nếu muốn sử dụng tính năng tạo luồng.-s EXPORT_ES6=1
sẽ chuyển mã JavaScript thành một mô-đun ES6 có chế độ xuất mặc định hoạt động với mọi trình kết hợp. Bạn cũng cần đặt-s MODULARIZE=1
.
Các cờ sau đây không phải lúc nào cũng cần thiết hoặc chỉ hữu ích cho mục đích gỡ lỗi:
-s FILESYSTEM=0
là một cờ liên quan đến Emscripten và khả năng mô phỏng hệ thống tệp cho bạn khi mã C/C++ của bạn sử dụng các thao tác trên hệ thống tệp. Nó phân tích mã mà nó biên dịch để quyết định có đưa tính năng mô phỏng hệ thống tệp vào mã kết dính hay không. Tuy nhiên, đôi khi, quá trình phân tích này có thể sai và bạn phải trả một khoản phí khá lớn là 70 kB cho mã kết dính bổ sung để mô phỏng hệ thống tệp mà bạn có thể không cần. Với-s FILESYSTEM=0
, bạn có thể buộc Emscripten không bao gồm mã này.-g4
sẽ khiến Emscripten đưa thông tin gỡ lỗi vào.wasm
và cũng phát ra một tệp bản đồ nguồn cho mô-đun wasm. Bạn có thể đọc thêm về việc gỡ lỗi bằng Emscripten trong phần gỡ lỗi của họ.
Vậy là xong! Để kiểm thử chế độ thiết lập này, hãy tạo một my-module.cpp
nhỏ:
#include <emscripten/bind.h>
using namespace emscripten;
int say_hello() {
printf("Hello from your wasm module\n");
return 0;
}
EMSCRIPTEN_BINDINGS(my_module) {
function("sayHello", &say_hello);
}
Và index.html
:
<!doctype html>
<title>Emscripten + npm example</title>
Open the console to see the output from the wasm module.
<script type="module">
import wasmModule from "./my-module.js";
const instance = wasmModule({
onRuntimeInitialized() {
instance.sayHello();
}
});
</script>
(Đây là một gist chứa tất cả các tệp.)
Để tạo mọi thứ, hãy chạy
$ npm install
$ npm run build
$ npm run serve
Khi chuyển đến localhost:8080, bạn sẽ thấy kết quả sau đây trong bảng điều khiển Công cụ cho nhà phát triển:

Thêm mã C/C++ làm phần phụ thuộc
Nếu muốn tạo một thư viện C/C++ cho ứng dụng web, bạn cần có mã của thư viện đó trong dự án. Bạn có thể thêm mã vào kho lưu trữ của dự án theo cách thủ công hoặc sử dụng npm để quản lý các loại phần phụ thuộc này. Giả sử tôi muốn sử dụng libvpx trong ứng dụng web của mình. libvpx là một thư viện C++ để mã hoá hình ảnh bằng VP8, bộ mã hoá và giải mã được dùng trong các tệp .webm
.
Tuy nhiên, libvpx không có trên npm và không có package.json
, nên tôi không thể cài đặt trực tiếp bằng npm.
Để giải quyết vấn đề này, bạn có thể dùng napa. napa cho phép bạn cài đặt mọi URL kho lưu trữ git làm phần phụ thuộc vào thư mục node_modules
.
Cài đặt napa làm phần phụ thuộc:
$ npm install --save napa
và nhớ chạy napa
dưới dạng tập lệnh cài đặt:
{
// ...
"scripts": {
"install": "napa",
// ...
},
"napa": {
"libvpx": "git+https://github.com/webmproject/libvpx"
}
// ...
}
Khi bạn chạy npm install
, napa sẽ lo việc sao chép kho lưu trữ libvpx GitHub vào node_modules
của bạn dưới tên libvpx
.
Giờ đây, bạn có thể mở rộng tập lệnh bản dựng để tạo libvpx. libvpx sử dụng configure
và make
để được tạo. May mắn thay, Emscripten có thể giúp đảm bảo rằng configure
và make
sử dụng trình biên dịch của Emscripten. Để phục vụ cho mục đích này, có các lệnh bao bọc emconfigure
và emmake
:
# ... above is unchanged ...
echo "============================================="
echo "Compiling libvpx"
echo "============================================="
(
rm -rf build-vpx || true
mkdir build-vpx
cd build-vpx
emconfigure ../node_modules/libvpx/configure \
--target=generic-gnu
emmake make
)
echo "============================================="
echo "Compiling libvpx done"
echo "============================================="
echo "============================================="
echo "Compiling wasm bindings"
echo "============================================="
# ... below is unchanged ...
Thư viện C/C++ được chia thành 2 phần: tiêu đề (thường là tệp .h
hoặc .hpp
) xác định các cấu trúc dữ liệu, lớp, hằng số, v.v. mà một thư viện hiển thị và thư viện thực tế (thường là tệp .so
hoặc .a
). Để dùng hằng số VPX_CODEC_ABI_VERSION
của thư viện trong mã, bạn phải thêm các tệp tiêu đề của thư viện bằng câu lệnh #include
:
#include "vpxenc.h"
#include <emscripten/bind.h>
int say_hello() {
printf("Hello from your wasm module with libvpx %d\n", VPX_CODEC_ABI_VERSION);
return 0;
}
Vấn đề là trình biên dịch không biết nơi cần tìm vpxenc.h
.
Đó là mục đích của cờ -I
. Nó cho trình biên dịch biết những thư mục cần kiểm tra tệp tiêu đề. Ngoài ra, bạn cũng cần cung cấp cho trình biên dịch tệp thư viện thực tế:
# ... above is unchanged ...
echo "============================================="
echo "Compiling wasm bindings"
echo "============================================="
(
# Compile C/C++ code
emcc \
${OPTIMIZE} \
--bind \
-s STRICT=1 \
-s ALLOW_MEMORY_GROWTH=1 \
-s ASSERTIONS=0 \
-s MALLOC=emmalloc \
-s MODULARIZE=1 \
-s EXPORT_ES6=1 \
-o ./my-module.js \
-I ./node_modules/libvpx \
src/my-module.cpp \
build-vpx/libvpx.a
# ... below is unchanged ...
Nếu chạy npm run build
ngay bây giờ, bạn sẽ thấy rằng quy trình này tạo một .js
mới và một tệp .wasm
mới, đồng thời trang minh hoạ sẽ thực sự xuất hằng số:

Bạn cũng sẽ nhận thấy rằng quá trình tạo bản dựng mất nhiều thời gian. Lý do khiến thời gian tạo bản dựng kéo dài có thể khác nhau. Trong trường hợp libvpx, quá trình này mất nhiều thời gian vì mỗi khi bạn chạy lệnh tạo bản dựng, hệ thống sẽ biên dịch một bộ mã hoá và một bộ giải mã cho cả VP8 và VP9, ngay cả khi các tệp nguồn không thay đổi. Ngay cả một thay đổi nhỏ đối với my-module.cpp
cũng sẽ mất nhiều thời gian để tạo. Việc giữ lại các tạo tác bản dựng của libvpx sẽ rất hữu ích sau khi chúng được tạo lần đầu tiên.
Một cách để đạt được điều này là sử dụng các biến môi trường.
# ... above is unchanged ...
eval $@
echo "============================================="
echo "Compiling libvpx"
echo "============================================="
test -n "$SKIP_LIBVPX" || (
rm -rf build-vpx || true
mkdir build-vpx
cd build-vpx
emconfigure ../node_modules/libvpx/configure \
--target=generic-gnu
emmake make
)
echo "============================================="
echo "Compiling libvpx done"
echo "============================================="
# ... below is unchanged ...
(Đây là một gist chứa tất cả các tệp.)
Lệnh eval
cho phép chúng ta đặt các biến môi trường bằng cách truyền các tham số vào tập lệnh bản dựng. Lệnh test
sẽ bỏ qua việc tạo libvpx nếu bạn đặt $SKIP_LIBVPX
(thành bất kỳ giá trị nào).
Giờ đây, bạn có thể biên dịch mô-đun của mình nhưng bỏ qua việc tạo lại libvpx:
$ npm run build:emscripten -- SKIP_LIBVPX=1
Tuỳ chỉnh môi trường tạo bản dựng
Đôi khi, các thư viện phụ thuộc vào các công cụ bổ sung để tạo. Nếu thiếu các phần phụ thuộc này trong môi trường xây dựng do hình ảnh Docker cung cấp, bạn cần tự thêm các phần phụ thuộc đó. Ví dụ: giả sử bạn cũng muốn tạo tài liệu về libvpx bằng doxygen. Doxygen không có trong vùng chứa Docker, nhưng bạn có thể cài đặt bằng cách sử dụng apt
.
Nếu làm như vậy trong build.sh
, bạn sẽ tải xuống và cài đặt lại doxygen mỗi khi muốn tạo thư viện. Việc này không chỉ lãng phí mà còn khiến bạn không thể làm việc trên dự án khi không có mạng.
Trong trường hợp này, bạn nên tạo hình ảnh Docker của riêng mình. Hình ảnh Docker được tạo bằng cách viết một Dockerfile
mô tả các bước tạo. Dockerfile khá mạnh mẽ và có rất nhiều lệnh, nhưng hầu hết thời gian bạn chỉ cần sử dụng FROM
, RUN
và ADD
. Trong trường hợp này:
FROM trzeci/emscripten
RUN apt-get update && \
apt-get install -qqy doxygen
Với FROM
, bạn có thể khai báo hình ảnh Docker mà bạn muốn dùng làm điểm bắt đầu. Tôi chọn trzeci/emscripten
làm cơ sở – hình ảnh mà bạn đã sử dụng từ trước đến nay. Với RUN
, bạn hướng dẫn Docker chạy các lệnh shell bên trong vùng chứa. Mọi thay đổi mà các lệnh này thực hiện đối với vùng chứa hiện là một phần của hình ảnh Docker. Để đảm bảo rằng hình ảnh Docker của bạn đã được tạo và có sẵn trước khi bạn chạy build.sh
, bạn phải điều chỉnh package.json
một chút:
{
// ...
"scripts": {
"build:dockerimage": "docker image inspect -f '.' mydockerimage || docker build -t mydockerimage .",
"build:emscripten": "docker run --rm -v $(pwd):/src mydockerimage ./build.sh",
"build": "npm run build:dockerimage && npm run build:emscripten && npm run build:app",
// ...
},
// ...
}
(Đây là một gist chứa tất cả các tệp.)
Thao tác này sẽ tạo hình ảnh Docker, nhưng chỉ khi hình ảnh đó chưa được tạo. Sau đó, mọi thứ sẽ chạy như trước, nhưng giờ đây, môi trường xây dựng có sẵn lệnh doxygen
. Lệnh này sẽ khiến tài liệu của libvpx cũng được tạo.
Kết luận
Không có gì ngạc nhiên khi mã C/C++ và npm không phù hợp với nhau, nhưng bạn có thể sử dụng chúng một cách khá thoải mái với một số công cụ bổ sung và khả năng cách ly mà Docker cung cấp. Chế độ thiết lập này sẽ không phù hợp với mọi dự án, nhưng đây là một điểm khởi đầu phù hợp mà bạn có thể điều chỉnh cho phù hợp với nhu cầu của mình. Nếu bạn có ý tưởng cải thiện, vui lòng chia sẻ.
Phụ lục: Tận dụng các lớp hình ảnh Docker
Một giải pháp thay thế là đóng gói nhiều vấn đề trong số này bằng Docker và phương pháp thông minh của Docker để lưu vào bộ nhớ đệm. Docker thực thi Dockerfile từng bước và chỉ định kết quả của mỗi bước là một hình ảnh riêng. Những hình ảnh trung gian này thường được gọi là "lớp". Nếu một lệnh trong Dockerfile không thay đổi, Docker sẽ không thực sự chạy lại bước đó khi bạn đang tạo lại Dockerfile. Thay vào đó, nó sẽ sử dụng lại lớp từ lần gần đây nhất hình ảnh được tạo.
Trước đây, bạn phải nỗ lực để không phải tạo lại libvpx mỗi khi tạo ứng dụng. Thay vào đó, bạn có thể di chuyển hướng dẫn tạo cho libvpx từ build.sh
sang Dockerfile
để tận dụng cơ chế lưu vào bộ nhớ đệm của Docker:
FROM trzeci/emscripten
RUN apt-get update && \
apt-get install -qqy doxygen git && \
mkdir -p /opt/libvpx/build && \
git clone https://github.com/webmproject/libvpx /opt/libvpx/src
RUN cd /opt/libvpx/build && \
emconfigure ../src/configure --target=generic-gnu && \
emmake make
(Đây là một gist chứa tất cả các tệp.)
Lưu ý rằng bạn cần cài đặt git theo cách thủ công và sao chép libvpx vì bạn không có các điểm gắn kết liên kết khi chạy docker build
. Do đó, bạn không cần dùng napa nữa.