Bạn tích hợp WebAssembly vào chế độ thiết lập này như thế nào? Trong bài viết này, chúng ta sẽ giải quyết vấn đề này với C/C++ và C/C++ làm ví dụ.
WebAssembly (wasm) thường được đóng khung là hiệu suất gốc hoặc cách chạy C++ hiện có của bạn cơ sở mã trên web. Với squoosh.app, chúng tôi muốn cho thấy có ít nhất một góc nhìn thứ ba về wasm: tận dụng việc khai thác hệ sinh thái của các ngôn ngữ lập trình khác. Bằng Emscripten, bạn có thể sử dụng mã C/C++, Rust có hỗ trợ wasm được tích hợp và Go nhóm cũng đang xử lý vấn đề này. Tôi và nhiều ngôn ngữ khác cũng sẽ được hỗ trợ.
Trong những trường hợp này, wasm không phải là trung tâm của ứng dụng mà là một câu đố mảnh: 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 xây dựng tập trung vào web và thậm chí có thể là một khung như React. Làm thế 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 tôi sẽ giải thích lấy C/C++ và Emscripten làm ví dụ.
Docker
Tôi thấy Docker là vô giá khi làm việc với Emscripten. C/C++ các thư viện thường được viết để tương thích với hệ điều hành mà chúng được xây dựng. Việc có được một môi trường nhất quán sẽ cực kỳ hữu ích. Với Docker, bạn sẽ có được hệ thống Linux ảo hóa đã được thiết lập để hoạt động với Emscripten và mọi công cụ và phần phụ thuộc đã cài đặt. Nếu thiếu nội dung nào đó, bạn có thể cài đặt mà không phải lo lắng về việc ứng dụng sẽ ảnh hưởng như thế nào đến máy tính của bạn hoặc các dự án khác. Nếu xảy ra sự cố, hãy vứt bỏ vùng chứa và bắt đầu qua. Nếu mã này hoạt động một lần, bạn có thể chắc chắn rằng mã sẽ tiếp tục hoạt động và sẽ cho ra kết quả giống hệt nhau.
Sổ đăng ký Docker có một Bản mô tả hình ảnh của trzeci mà tôi đang sử dụng rộng rãi.
Tích hợp với npm
Trong phần lớn các trường hợp, điểm truy cập vào dự án web là npm
package.json
. Theo quy ước, bạn có thể xây dựng hầu hết các dự án 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 (một .js
và một .wasm
tệp) nên được coi là một mô-đun JavaScript khác và chỉ
nội dung. Tệp JavaScript có thể do một gói như webpack hoặc cuộn lên xử lý,
và tệp wasm nên được xử lý giống như mọi tài sản nhị phân lớn hơn khác, chẳng hạn như
hình ảnh.
Do đó, các cấu phần phần mềm bản dựng Emscripten cần được tạo trước khi chuyển sang trạng thái "bình thường" quá trình xây dựng sẽ khởi động:
{
"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ể gọi trực tiếp Emscripten, nhưng vì
đề cập ở trên, bạn nên sử dụng Docker để đảm bảo môi trường tạo bản dựng
nhất quán.
docker run ... trzeci/emscripten ./build.sh
yêu cầu Docker khởi động một thiết bị mới
vùng chứa bằng hình ảnh trzeci/emscripten
rồi chạy lệnh ./build.sh
.
build.sh
là tập lệnh shell mà bạn sẽ viết tiếp theo! --rm
cho biết
Docker để xoá vùng chứa sau khi chạy xong. Bằng cách này, bạn không phải tạo
tạo một tập hợp các hình ảnh máy cũ theo thời gian. -v $(pwd):/src
có nghĩa là
bạn muốn Docker trở thành "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 ở thư mục /src
bên trong
vùng chứa sẽ được đồng bộ hoá với dự án thực tế của bạn. Các thư mục được đồng bộ hoá hai chiều này
được gọi là "liên kết gắn kết".
Hãy cùng tìm hiểu về 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 để phân tích ở đây!
set -e
đặt shell vào trạng thái "không thành công nhanh chóng" . Nếu có bất kỳ lệnh nào trong tập lệnh
trả về một lỗi, thì toàn bộ tập lệnh sẽ bị huỷ ngay lập tức. Thông tin này có thể là
vô cùng hữu ích vì kết quả cuối cùng của tập lệnh sẽ luôn thành công
hoặc lỗi khiến bản dựng không thành công.
Với câu lệnh export
, bạn xác định giá trị của một vài môi trường
biến. Chúng cho phép bạn truyền các tham số dòng lệnh bổ sung đến C
(CFLAGS
), trình biên dịch C++ (CXXFLAGS
) và trình liên kết (LDFLAGS
).
Tất cả họ đều nhận được chế độ cài đặt trình tối ưu hoá qua OPTIMIZE
để đảm bảo rằng
mọi thứ đều được tối ưu hoá theo cách tương tự. Có một vài giá trị có thể
cho biến OPTIMIZE
:
-O0
: Không thực hiện bất kỳ tối ưu hoá nào. Không loại bỏ mã chết nào và Emscripten cũng không thu nhỏ mã JavaScript mà nó phát ra. Phù hợp để gỡ lỗi.-O3
: Tối ưu hoá tích cực để đạt hiệu suất.-Os
: Tối ưu hoá tích cực để nâng cao hiệu suất và quy mô dưới dạng chiến dịch phụ tiêu chí.-Oz
: Tối ưu hoá tích cực để tăng quy mô, giảm 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 là
là tính năng "thay thế cho các trình biên dịch như GCC hoặc clang". Vậy tất cả
cờ mà bạn có thể biết từ GCC rất có thể sẽ được emcc triển khai dưới dạng
tốt. Cờ -s
đặc biệt ở chỗ nó cho phép chúng ta định cấu hình Emscripten
cụ thể. Tất cả các lựa chọn có sẵn trong trình đơn Emscripten
settings.js
!
nhưng tệp đó có thể gây quá tải. Dưới đây là danh sách các cờ Emscripten
mà tôi nghĩ là quan trọng nhất đối với nhà phát triển web:
--bind
bật liên kết.-s STRICT=1
ngừng hỗ trợ mọi tuỳ chọn bản dựng không dùng nữa. Điều này giúp đảm bảo mà mã của bạn tạo theo cách tương thích chuyển tiếp.-s ALLOW_MEMORY_GROWTH=1
cho phép tự động tăng bộ nhớ nếu nếu cần. Tại thời điểm viết, Emscripten sẽ phân bổ 16MB bộ nhớ ban đầu. Khi mã của bạn phân bổ các đoạn bộ nhớ, tuỳ chọn này sẽ quyết định xem các thao tác này sẽ làm cho toàn bộ mô-đun wasm không thành công khi bộ nhớ hoặc nếu mã kết nối được phép mở rộng tổng bộ nhớ lên phù hợp với sự phân bổ.-s MALLOC=...
chọn cách triển khaimalloc()
để sử dụng.emmalloc
là một cách triển khaimalloc()
nhỏ và nhanh chóng dành riêng cho Emscripten. Chiến lược phát hành đĩa đơn thay thế làdlmalloc
, một phương thức triển khaimalloc()
chính thức. Chỉ bạn cần chuyển sangdlmalloc
nếu bạn đang phân bổ nhiều đối tượng nhỏ thường xuyên hoặc nếu bạn muốn sử dụng luồng.-s EXPORT_ES6=1
sẽ chuyển mã JavaScript thành mô-đun ES6 có xuất dữ liệu mặc định hoạt động với bất kỳ gói nào. Cũng cần-s MODULARIZE=1
để thiết lập.
Các cờ sau không phải lúc nào cũng cần thiết hoặc chỉ hữu ích khi gỡ lỗi mục đích:
-s FILESYSTEM=0
là một cờ có liên quan đến Emscripten và có 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 thao tác hệ thống tệp. Công cụ này thực hiện một số phân tích trên mã mà nó biên dịch để quyết định xem có bao gồm mô phỏng hệ thống tệp trong mã keo. Tuy nhiên, đôi khi thì bản phân tích có thể sai và bạn phải trả 70kB khá nhiều để có thêm công cụ phân tích để mô phỏng hệ thống tệp mà bạn có thể không cần đến. Với-s FILESYSTEM=0
, bạn có thể buộc Emscripten không thêm mã này vào.-g4
sẽ làm cho Emscripten bao gồm thông tin gỡ lỗi trong.wasm
và cũng phát ra tệp bản đồ nguồn cho mô-đun wasm. Bạn có thể đọc thêm trên gỡ lỗi bằng Emscripten trong quy trình gỡ lỗi .
Vậy là xong! Để kiểm tra chế độ thiết lập này, hãy tạo một my-module.cpp
nhỏ nhắn:
#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à một 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à ý chính 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 điều hướng đến localhost:8080, bạn sẽ thấy kết quả sau trong Bảng điều khiển Công cụ cho nhà phát triển:
![Công cụ cho nhà phát triển cho thấy thông báo được in qua C++ và Emscripten.](https://web.dev/static/articles/emscripten-npm/image/devtools-showing-message-6e40fef465a41.png?authuser=5&hl=vi)
Thêm mã C/C++ làm phần phụ thuộc
Nếu muốn xây dựng thư viện C/C++ cho ứng dụng web, bạn cần có mã của thư viện đó
trong dự án của bạn. Bạn có thể tự thêm mã nguồn vào kho lưu trữ của dự án
hoặc bạn cũng có thể 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 tôi. libvpx
là thư viện C++ để mã hoá hình ảnh bằng VP8, bộ mã hoá và giải mã được dùng trong tệp .webm
.
Tuy nhiên, libvpx không có trên npm và không có package.json
, vì vậy tôi không thể
bằng cách sử dụng npm trực tiếp.
Để thoát khỏi câu hỏi hóc búa này, có
napa. napa cho phép bạn cài đặt bất kỳ git
URL kho lưu trữ dưới dạng phần phụ thuộc vào thư mục node_modules
.
Cài đặt napa dưới dạng 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ẽ xử lý việc sao chép libvpx GitHub
kho lưu trữ 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
để xây dựng. 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ụ mục đích này, có trình bao bọc
các lệnh 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 hai phần: phần đầu (theo truyền thống là .h
hoặc
.hpp
) xác định 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ế (theo truyền thống là các tệp .so
hoặc .a
). Người nhận
sử dụng hằng số VPX_CODEC_ABI_VERSION
của thư viện trong mã, bạn có
để đưa các tệp tiêu đề của thư viện vào 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 ở đâu để tìm vpxenc.h
.
Đây là mục đích của cờ -I
. Phương thức này cho trình biên dịch biết cần chọn thư mục nào
hãy 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 quá trình này sẽ tạo một .js
mới
và một tệp .wasm
mới và trang minh hoạ sẽ thực sự xuất ra hằng số:
![DevTools
cho thấy phiên bản ABI của libvpx được in qua emscripten.](https://web.dev/static/articles/emscripten-npm/image/devtools-showing-the-abi-ab9a0f24d2c5c.png?authuser=5&hl=vi)
Bạn cũng sẽ nhận thấy rằng quá trình xây dựng mất nhiều thời gian. Lý do
thời gian xây dựng 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ì
nó biên dịch một bộ mã hoá và một bộ giải mã cho cả VP8 và VP9 mỗi khi bạn chạy
lệnh tạo bản dựng của bạn, mặc dù các tệp nguồn không thay đổi. Ngay cả khi một quy mô nhỏ
thì việc thay đổi đối với my-module.cpp
sẽ mất nhiều thời gian. Sẽ rất hữu ích
sẽ giúp ích cho việc lưu giữ các cấu phần phần mềm bản dựng của libvpx sau khi chúng được
tạo được 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 ...
(Sau đây là ý chính 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
$SKIP_LIBVPX
được đặt (thành bất kỳ giá trị nào).
Bây giờ, 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, thư viện phụ thuộc vào các công cụ bổ sung để xây dựng. Nếu các phần phụ thuộc này
bị thiếu trong môi trường xây dựng do hình ảnh Docker cung cấp, bạn cần
tự thêm chúng. Ví dụ: giả sử bạn cũng muốn tạo
tài liệu về libvpx bằng doxygen. Doxygen thì không
có sẵn bên trong vùng chứa Docker, nhưng bạn có thể cài đặt bằng apt
.
Nếu thực hiện việc đó trong build.sh
, bạn cần tải xuống rồi cài đặt lại
doxygen mỗi khi bạn muốn xây dựng thư viện. Điều đó không chỉ
nhưng điều này cũng sẽ cản trở bạn thực hiện dự án khi không có kết nối Internet.
Ở đây, bạn nên tạo hình ảnh Docker của riêng mình. Docker hình ảnh được tạo bởi
viết một Dockerfile
mô tả các bước tạo. Dockerfile khá
mạnh mẽ và có rất nhiều
nhưng hầu hết
thời gian bạn có thể bỏ qua chỉ bằ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 sử dụng khi bắt đầu
điểm. Tôi đã chọn trzeci/emscripten
làm cơ sở — hình ảnh bạn đang sử dụng
tất cả đều ổn. Với RUN
, bạn hướng dẫn Docker chạy các lệnh shell bên trong
vùng chứa. Bất kể thay đổi nào mà những lệnh này thực hiện đối với vùng chứa giờ đây là một phần của
hình ảnh Docker. Để đảm bảo hình ảnh Docker của bạn đã được tạo và đang
có sẵn trước khi chạy build.sh
, bạn phải điều chỉnh package.json
của mình
bit:
{
// ...
"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",
// ...
},
// ...
}
(Sau đây là ý chính 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 chưa được tạo. Sau đó
mọi thứ chạy như trước, nhưng giờ đây, môi trường tạo bản dựng có doxygen
Lệnh này sẽ khiến tài liệu của libvpx được tạo dưới dạng
tốt.
Kết luận
Không có gì đáng ngạc nhiên khi mã C/C++ và npm không phù hợp một cách tự nhiên, nhưng bạn có thể giúp ứng dụng hoạt động khá thoải mái với một số công cụ bổ sung và khả năng tách biệt mà Docker cung cấp. Chế độ thiết lập này không áp dụng cho mọi dự án, nhưng 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ó cải tiến, vui lòng chia sẻ.
Phụ lục: Tận dụng các lớp hình ảnh của Docker
Một giải pháp thay thế là đóng gói nhiều vấn đề hơn trong số này bằng Docker và Phương pháp lưu vào bộ nhớ đệm thông minh của Docker. Docker thực thi Dockerfiles từng bước và gán kết quả của mỗi bước một hình ảnh của riêng bước đó. 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 chạy lại bước đó khi bạn tạo lại Dockerfile. Thay vào đó lớp này sẽ sử dụng lại lớp từ lần cuối cùng hình ảnh được tạo.
Trước đây, bạn phải nỗ lực rất nhiều để không tạo lại libvpx mỗi lần
bạn xây dựng ứng dụng của mình. 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 tính năng lưu vào bộ nhớ đệm của Docker
cơ chế:
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
(Sau đây là ý chính chứa tất cả các tệp).
Lưu ý rằng bạn cần cài đặt git và sao chép libvpx theo cách thủ công vì bạn không có
liên kết gắn kết khi chạy docker build
. Như là hiệu ứng phụ, không cần
napa nữa.