Đôi khi, bạn muốn sử dụng thư viện chỉ có sẵn dưới dạng mã C hoặc C++. Thông thường, đây là lúc bạn nên từ bỏ. À, không còn nữa, vì bây giờ chúng tôi có Emscripten và WebAssembly (hoặc Wasm)!
Chuỗi công cụ
Tôi đặt mục tiêu cho bản thân là tìm ra cách biên dịch một số mã C hiện có để Wasm. Có một số tiếng ồn xung quanh phần phụ trợ Wasm của LLVM, vì vậy Tôi bắt đầu tìm hiểu sâu về điều đó. Trong khi bạn có thể tải các chương trình đơn giản để biên dịch theo cách này, lần thứ hai bạn muốn sử dụng thư viện chuẩn của C hoặc thậm chí biên dịch nhiều tệp, bạn có thể sẽ gặp sự cố. Điều này đưa tôi đến bài học tôi đã học được:
Mặc dù Emscripten được dùng làm trình biên dịch C-to-asm.js, nhưng kể từ đó Emscripten đã phát triển thành mục tiêu Wasm và là trong quá trình chuyển đổi vào phần phụ trợ LLVM chính thức trong nội bộ. Emscripten cũng cung cấp Cách triển khai tương thích với Wasm của thư viện chuẩn của C. Sử dụng Emscripten. Nó thực hiện rất nhiều công việc ẩn giấu, mô phỏng hệ thống tệp, cung cấp tính năng quản lý bộ nhớ, kết hợp OpenGL với WebGL — một nhiều thứ mà bạn thực sự không cần trải nghiệm khi phát triển cho chính mình.
Mặc dù bạn có vẻ phải lo lắng về việc cồng kềnh nhưng chắc chắn tôi cũng lo lắng — trình biên dịch Emscripten sẽ xoá mọi thứ không cần thiết. Trong các thử nghiệm, các mô-đun Wasm thu được sẽ có kích thước phù hợp cho logic mà chúng chứa, cũng như các nhóm Emscripten và WebAssembly đang nỗ lực tạo ra nhỏ hơn nữa trong tương lai.
Bạn có thể tải Emscripten bằng cách làm theo hướng dẫn trên trang web hoặc thông qua Homebrew. Nếu bạn là người hâm mộ của các lệnh được lưu trữ như tôi và không muốn cài đặt mọi thứ trên hệ thống của bạn chơi với WebAssembly, có một ứng dụng Hình ảnh Docker mà bạn có thể sử dụng thay vào đó:
$ docker pull trzeci/emscripten
$ docker run --rm -v $(pwd):/src trzeci/emscripten emcc <emcc options here>
Biên dịch một nội dung đơn giản
Hãy xem ví dụ gần như chuẩn hoá về việc viết một hàm trong C mà tính số fibonacci thứ n:
#include <emscripten.h>
EMSCRIPTEN_KEEPALIVE
int fib(int n) {
if(n <= 0){
return 0;
}
int i, t, a = 0, b = 1;
for (i = 1; i < n; i++) {
t = a + b;
a = b;
b = t;
}
return b;
}
Nếu bạn biết C, hàm này không nên quá bất ngờ. Ngay cả khi bạn không biết C nhưng biết JavaScript, hy vọng bạn sẽ có thể hiểu điều gì đang xảy ra ở đây.
emscripten.h
là tệp tiêu đề do Emscripten cung cấp. Chúng tôi chỉ cần vậy nên chúng tôi
có quyền truy cập vào macro EMSCRIPTEN_KEEPALIVE
, nhưng nó
cung cấp nhiều chức năng hơn.
Macro này yêu cầu trình biên dịch không xoá một hàm ngay cả khi hàm đó xuất hiện
không sử dụng. Nếu chúng ta bỏ qua macro đó, trình biên dịch sẽ tối ưu hoá hàm đó
— rốt cuộc thì không có ai sử dụng nó.
Hãy lưu tất cả những dữ liệu đó trong một tệp có tên là fib.c
. Để chuyển thành tệp .wasm
, chúng ta
cần chuyển sang lệnh trình biên dịch emcc
của Emscripten:
$ emcc -O3 -s WASM=1 -s EXTRA_EXPORTED_RUNTIME_METHODS='["cwrap"]' fib.c
Hãy cùng phân tích lệnh này. emcc
là trình biên dịch của Emscripten. fib.c
là C của chúng ta
. Đến giờ thì mọi thứ vẫn ổn! -s WASM=1
yêu cầu Emscripten cung cấp cho chúng ta một tệp Wasm
thay vì tệp asm.js.
-s EXTRA_EXPORTED_RUNTIME_METHODS='["cwrap"]'
yêu cầu trình biên dịch để lại
Hàm cwrap()
có sẵn trong tệp JavaScript — nội dung khác về hàm này
sau. -O3
yêu cầu trình biên dịch tối ưu hoá một cách linh hoạt. Bạn có thể chọn thấp hơn
các số liệu để giảm thời gian xây dựng, nhưng điều đó cũng sẽ khiến các gói kết quả
lớn hơn vì trình biên dịch có thể không xoá mã không dùng đến.
Sau khi chạy lệnh, bạn sẽ kết thúc với một tệp JavaScript có tên
a.out.js
và một tệp WebAssembly có tên a.out.wasm
. Tệp Wasm (hoặc
"mô-đun") chứa mã C được biên dịch và có kích thước khá nhỏ. Chiến lược phát hành đĩa đơn
Tệp JavaScript đảm nhận việc tải và khởi tạo mô-đun Wasm và
cung cấp API đẹp hơn. Nếu cần, hệ thống cũng sẽ đảm nhận việc thiết lập
ngăn xếp, bộ nhớ khối xếp và các chức năng khác thường được cung cấp bởi
hệ điều hành khi viết mã C. Do đó, tệp JavaScript có
lớn hơn, có trọng lượng 19KB (~5KB gzip).
Chạy một điều đơn giản
Cách dễ nhất để tải và chạy mô-đun của bạn là sử dụng JavaScript đã tạo
. Sau khi tải tệp đó, bạn sẽ có
Module
toàn cầu
theo ý bạn. Sử dụng
cwrap
để tạo hàm gốc JavaScript đảm nhận việc chuyển đổi các tham số
thành tên nào đó thân thiện với C và gọi hàm được bao bọc. cwrap
sẽ đảm nhận
tên hàm, kiểu dữ liệu trả về và kiểu đối số dưới dạng đối số, theo thứ tự đó:
<script src="a.out.js"></script>
<script>
Module.onRuntimeInitialized = _ => {
const fib = Module.cwrap('fib', 'number', ['number']);
console.log(fib(12));
};
</script>
Nếu bạn chạy mã này, bạn sẽ thấy "144" trong bảng điều khiển, tức là số Fibonacci thứ 12.
Khuyến mãi: Biên dịch thư viện C
Cho đến bây giờ, mã C mà chúng tôi viết luôn chú trọng đến Wasm. Lõi trường hợp sử dụng của WebAssembly là lấy hệ sinh thái C hiện có thư viện và cho phép nhà phát triển sử dụng chúng trên web. Những thư viện này thường dựa vào thư viện chuẩn của C, hệ điều hành, hệ thống tệp và các của bạn. Emscripten cung cấp hầu hết các tính năng này, mặc dù có một số hạn chế.
Hãy quay lại mục tiêu ban đầu của tôi: biên dịch bộ mã hoá cho WebP thành Wasm. Chiến lược phát hành đĩa đơn mã nguồn của bộ mã hoá và giải mã WebP được viết bằng C và có trên GitHub cũng như một số nguồn Tài liệu API. Đó là một xuất phát điểm khá tốt.
$ git clone https://github.com/webmproject/libwebp
Để bắt đầu, hãy thử hiển thị WebPGetEncoderVersion()
từ
encode.h
sang JavaScript bằng cách ghi một tệp C có tên webp.c
:
#include "emscripten.h"
#include "src/webp/encode.h"
EMSCRIPTEN_KEEPALIVE
int version() {
return WebPGetEncoderVersion();
}
Đây là một chương trình đơn giản tốt để kiểm tra xem chúng tôi có thể lấy được mã nguồn của libwebp hay không để biên dịch, vì chúng tôi không yêu cầu bất kỳ tham số hoặc cấu trúc dữ liệu phức tạp nào để gọi hàm này.
Để biên dịch chương trình này, chúng ta cần cho trình biên dịch biết nơi có thể tìm thấy
các tệp tiêu đề của libwebp bằng cách sử dụng cờ -I
và cũng chuyển vào đó tất cả các tệp C của
libwebp mà ứng dụng cần. Tôi thành thật: Tôi chỉ trao tất cả chữ C
các tệp mà tôi có thể tìm thấy và dựa vào trình biên dịch để loại bỏ mọi tệp
không cần thiết. Có vẻ như nó đã hoạt động rất hiệu quả!
$ emcc -O3 -s WASM=1 -s EXTRA_EXPORTED_RUNTIME_METHODS='["cwrap"]' \
-I libwebp \
webp.c \
libwebp/src/{dec,dsp,demux,enc,mux,utils}/*.c
Giờ đây, chúng ta chỉ cần một số HTML và JavaScript để tải mô-đun mới nổi bật của mình:
<script src="/a.out.js"></script>
<script>
Module.onRuntimeInitialized = async (_) => {
const api = {
version: Module.cwrap('version', 'number', []),
};
console.log(api.version());
};
</script>
Và chúng ta sẽ thấy số phiên bản chỉnh sửa trong đầu ra:
Đưa hình ảnh từ JavaScript vào Wasm
Việc lấy số phiên bản của bộ mã hoá rất hữu ích, nhưng việc mã hoá phiên bản hình ảnh sẽ ấn tượng hơn, đúng không? Vậy chúng ta bắt đầu tìm hiểu nhé.
Câu hỏi đầu tiên chúng ta phải trả lời là: Làm cách nào để đưa hình ảnh vào Vùng đất Wasm?
Nhìn vào
mã hoá API của libwebp, dự kiến
một mảng byte theo RGB, RGBA, BGR hoặc BGRA. May mắn thay, Canvas API đã có
getImageData()
!
đem lại cho chúng tôi
Uint8ClampedArray
chứa dữ liệu hình ảnh theo RGBA:
async function loadImage(src) {
// Load image
const imgBlob = await fetch(src).then((resp) => resp.blob());
const img = await createImageBitmap(imgBlob);
// Make canvas same size as image
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
// Draw image onto canvas
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
return ctx.getImageData(0, 0, img.width, img.height);
}
Giờ đây, giá trị này sẽ "chỉ" vấn đề sao chép dữ liệu từ vùng đất JavaScript vào Wasm đất liền. Do đó, chúng ta cần hiển thị 2 hàm bổ sung. Một chiến dịch phân bổ cho hình ảnh bên trong Wasm land và một bộ nhớ đã giải phóng hình ảnh đó một lần nữa:
EMSCRIPTEN_KEEPALIVE
uint8_t* create_buffer(int width, int height) {
return malloc(width * height * 4 * sizeof(uint8_t));
}
EMSCRIPTEN_KEEPALIVE
void destroy_buffer(uint8_t* p) {
free(p);
}
create_buffer
phân bổ một vùng đệm cho hình ảnh RGBA — do đó, 4 byte cho mỗi pixel.
Con trỏ do malloc()
trả về là địa chỉ ô bộ nhớ đầu tiên của
vùng đệm đó. Khi con trỏ được trả về vùng JavaScript, nó sẽ được coi là
chỉ một con số. Sau khi hiển thị hàm cho JavaScript bằng cwrap
, chúng ta có thể
sử dụng số đó để tìm điểm bắt đầu của vùng đệm và sao chép dữ liệu hình ảnh.
const api = {
version: Module.cwrap('version', 'number', []),
create_buffer: Module.cwrap('create_buffer', 'number', ['number', 'number']),
destroy_buffer: Module.cwrap('destroy_buffer', '', ['number']),
};
const image = await loadImage('/image.jpg');
const p = api.create_buffer(image.width, image.height);
Module.HEAP8.set(image.data, p);
// ... call encoder ...
api.destroy_buffer(p);
Chung cuộc: Mã hoá hình ảnh
Hình ảnh hiện đã có ở Vùng đất Wasm. Đã đến lúc gọi bộ mã hoá WebP để
phát huy tác dụng! Nhìn vào
Tài liệu WebP, WebPEncodeRGBA
có vẻ hoàn toàn phù hợp. Hàm này đưa con trỏ đến hình ảnh đầu vào và
kích thước, cũng như tuỳ chọn chất lượng từ 0 đến 100. Chiến lược này cũng phân bổ
vùng đệm đầu ra mà chúng tôi cần giải phóng bằng WebPFree()
sau khi
thực hiện xong với hình ảnh WebP.
Kết quả của thao tác mã hoá là vùng đệm đầu ra và độ dài của vùng đệm đó. Bởi vì hàm trong C không thể chứa các mảng làm kiểu dữ liệu trả về (trừ phi chúng ta phân bổ bộ nhớ động), tôi đã sử dụng một mảng toàn cục tĩnh. Tôi biết, không phải C sạch (thực tế là nó dựa trên thực tế là con trỏ Wasm có chiều rộng 32 bit), nhưng để giữ mọi thứ đơn giản, tôi nghĩ đây là một lối tắt hợp lý.
int result[2];
EMSCRIPTEN_KEEPALIVE
void encode(uint8_t* img_in, int width, int height, float quality) {
uint8_t* img_out;
size_t size;
size = WebPEncodeRGBA(img_in, width, height, width * 4, quality, &img_out);
result[0] = (int)img_out;
result[1] = size;
}
EMSCRIPTEN_KEEPALIVE
void free_result(uint8_t* result) {
WebPFree(result);
}
EMSCRIPTEN_KEEPALIVE
int get_result_pointer() {
return result[0];
}
EMSCRIPTEN_KEEPALIVE
int get_result_size() {
return result[1];
}
Bây giờ, với tất cả những điều đó, chúng ta có thể gọi hàm mã hoá, lấy giá trị con trỏ và kích thước hình ảnh, đặt nó vào vùng đệm JavaScript của riêng chúng tôi và giải phóng tất cả vùng đệm Wasm-land mà chúng tôi đã phân bổ trong quá trình này.
api.encode(p, image.width, image.height, 100);
const resultPointer = api.get_result_pointer();
const resultSize = api.get_result_size();
const resultView = new Uint8Array(Module.HEAP8.buffer, resultPointer, resultSize);
const result = new Uint8Array(resultView);
api.free_result(resultPointer);
Tuỳ thuộc vào kích thước hình ảnh, bạn có thể gặp lỗi khi Wasm không thể tăng bộ nhớ đủ để chứa cả hình ảnh đầu vào và đầu ra:
May mắn thay, giải pháp cho vấn đề này nằm trong thông báo lỗi! Chúng tôi chỉ cần
thêm -s ALLOW_MEMORY_GROWTH=1
vào lệnh biên dịch.
Vậy là xong! Chúng tôi biên dịch một bộ mã hoá WebP và chuyển mã hình ảnh JPEG thành
WebP. Để chứng minh rằng cách làm này hoạt động, chúng ta có thể chuyển vùng đệm kết quả thành một blob và sử dụng
trên phần tử <img>
:
const blob = new Blob([result], { type: 'image/webp' });
const blobURL = URL.createObjectURL(blob);
const img = document.createElement('img');
img.src = blobURL;
document.body.appendChild(img);
Xin giới thiệu vô cùng lợi ích của hình ảnh WebP mới!
Kết luận
Để thư viện C hoạt động trong trình duyệt không phải là đi bộ trong công viên, mà một khi hiểu được toàn bộ quy trình và cách luồng dữ liệu hoạt động. dễ dàng hơn và kết quả có thể cực kỳ ấn tượng.
WebAssembly mở ra nhiều khả năng mới trên web cho việc xử lý, số lượng chơi game và kiểm tra kiến thức. Hãy nhớ rằng Wasm không phải là một giải pháp hoàn hảo được áp dụng cho mọi thứ, nhưng khi bạn gặp phải một trong những nút thắt cổ chai đó, Wasm có thể một công cụ cực kỳ hữu ích.
Nội dung bổ trợ: Chạy điều gì đó đơn giản nhưng khó khăn
Nếu muốn thử và tránh tệp JavaScript đã tạo, bạn có thể sang. Hãy quay lại ví dụ về Fibonacci. Để tự tải và chạy chương trình này, chúng tôi có thể hãy làm như sau:
<!DOCTYPE html>
<script>
(async function () {
const imports = {
env: {
memory: new WebAssembly.Memory({ initial: 1 }),
STACKTOP: 0,
},
};
const { instance } = await WebAssembly.instantiateStreaming(
fetch('/a.out.wasm'),
imports,
);
console.log(instance.exports._fib(12));
})();
</script>
Các mô-đun WebAssembly do Emscripten tạo không có bộ nhớ để hoạt động
trừ phi bạn cung cấp bộ nhớ cho chúng. Cách cung cấp mô-đun Wasm
bất cứ thứ gì đều bằng cách sử dụng đối tượng imports
— tham số thứ hai của
instantiateStreaming
. Mô-đun Wasm có thể truy cập vào mọi nội dung bên trong
đối tượng nhập, nhưng không có gì khác bên ngoài đối tượng đó. Theo quy ước, mô-đun
được biên dịch bởi Emscripting mong đợi một số điều từ JavaScript đang tải
môi trường:
- Đầu tiên là
env.memory
. Mô-đun Wasm không nhận biết được bên ngoài thế giới xung quanh nên cần phải có một số bộ nhớ để hoạt động. Vào cảnhWebAssembly.Memory
. Lớp này đại diện cho một phần (không bắt buộc) của bộ nhớ tuyến tính. Kích thước các thông số được tính bằng "tính bằng đơn vị của trang WebAssembly", nghĩa là mã ở trên phân bổ 1 trang bộ nhớ, với mỗi trang có kích thước là 64 KiB. Không cung cấpmaximum
về mặt lý thuyết, bộ nhớ không bị giới hạn trong sự tăng trưởng (Chrome hiện tại giới hạn cố định là 2GB). Hầu hết các mô-đun WebAssembly không cần phải đặt tối đa. env.STACKTOP
xác định vị trí ngăn xếp bắt đầu phát triển. Nhóm ảnh là cần thiết để thực hiện lệnh gọi hàm và phân bổ bộ nhớ cho các biến cục bộ. Vì chúng tôi không thực hiện bất kỳ hành vi quản lý bộ nhớ linh động nào trong Fibonacci, chúng ta có thể chỉ sử dụng toàn bộ bộ nhớ làm ngăn xếp, do đóSTACKTOP = 0
.