Biên dịch mkbitmap thành WebAssembly

Trong bài viết WebAssembly là gì và nguồn gốc của nó, Tôi đã giải thích cách chúng tôi kết thúc việc sử dụng WebAssembly ngày hôm nay. Trong bài viết này, tôi sẽ giới thiệu cho bạn phương pháp biên dịch một chương trình C hiện có (mkbitmap) cho WebAssembly. Thao tác này phức tạp hơn ví dụ về Hello world, vì trong đó người dùng có thể làm việc với các tệp, giao tiếp giữa các trang WebAssembly và JavaScript, cũng như vẽ lên canvas, nhưng vẫn dễ quản lý để không làm bạn bị choáng ngợp.

Bài viết này dành cho các nhà phát triển web muốn tìm hiểu WebAssembly và trình bày hướng dẫn từng bước nếu bạn muốn biên dịch một nội dung như mkbitmap thành WebAssembly. Xin nhắc lại là việc không biên dịch ứ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 một số bước được mô tả dưới đây không hoạt động, vì vậy tôi cần quay lại và thử lại theo cách khác. Bài viết này không cho thấy lệnh biên dịch cuối cùng ma thuật như thể nó rơi xuống từ trên trời, mà mô tả tiến trình thực tế của tôi, bao gồm cả một số phiền toái.

Khoảng mkbitmap

Chương trình C mkbitmap đọc hình ảnh và áp dụng một hoặc nhiều thao tác sau cho hình ảnh theo thứ tự sau: đảo ngược, lọc thông cao, chia tỷ lệ và tạo ngưỡng. Bạn có thể kiểm soát và bật/tắt từng hoạt động một cách riêng biệt. Mục đích chính của mkbitmap là chuyển đổi hình ảnh màu hoặc thang màu xám sang đị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ụ xử lý trước, mkbitmap đặc biệt hữu ích trong việc chuyển đổi nghệ thuật đường nét đã 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 vào đó một số tuỳ 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 của công cụ này:

$ mkbitmap [options] [filename...]
Hình ảnh hoạt hình có màu.
Hình ảnh gốc (Nguồn).
Hình ảnh hoạt hình được chuyển đổi sang thang màu xám sau khi xử lý trước.
Tăng tỷ lệ lần đầu, sau đó đạt ngưỡng: 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 mã 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ụ trên thiết bị để cảm nhận về hành vi của công cụ. Tệp INSTALL chứa những hướng dẫn sau:

  1. cd vào thư mục chứa mã nguồn của gói và nhập ./configure để định cấu hình gói này 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 tính năng nào đang được kiểm tra.

  2. Nhập make để biên dịch gói này.

  3. Bạn có thể tuỳ ý 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 gỡ cài đặt vừa tạo.

  4. Nhập make install để cài đặt các chương trình cũng như mọi tệp dữ liệu và tài liệu. Khi cài đặt vào một tiền tố do gốc sở hữu, 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 và chỉ thực thi giai đoạn make install bằng các đặc quyền gốc.

Bằng cách làm theo các bước này, bạn sẽ có hai tệp thực thi, potracemkbitmap – trọng tâm của bài viết này. Bạn có thể xác minh rằng mã này hoạt động đúng cách bằng cách chạy mkbitmap --version. Đây là kết quả của cả bốn bước từ máy của tôi, được cắt ngắn rất nhiều để 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 cách 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 lấy đượ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 với các bước này để sử dụng WebAssembly.

Biên dịch mkbitmap thành WebAssembly

Emscripten là một công cụ để biên dịch các chương trình C/C++ thành WebAssembly. Tài liệu về Building Project (Dự án xây dựng) của Emscripten trình bày những thông tin sau:

Xây dựng 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 phương thức thay thế thả xuống cho gcc – 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 của dự án vẫn không thay đổi.

Sau đó, tài liệu sẽ tiếp tục (chỉnh sửa một chút cho ngắn gọn):

Hãy xem xét trường hợp bạn thường tạo bằng các lệnh sau:

./configure
make

Để tạo bằng Emscripten, bạn sẽ dùng các lệnh sau:

emconfigure ./configure
emmake make

Vì vậy, về cơ bản, ./configure trở thành emconfigure ./configuremake trở thành emmake make. Phần sau đây minh hoạ 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ó tệp .wasm ở đâu đó trong thư mục. Bạn có thể tìm thấy các trình xử lý này bằng cách chạy find . -name "*.wasm":

$ find . -name "*.wasm"
./a.wasm
./src/mkbitmap.wasm
./src/potrace.wasm

2 phần tử 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ó hai tệp mới tương ứng là mkbitmappotrace. Trong bài viết này, chỉ mkbitmap là phù hợp. Việc các tệp này không có đuôi .js hơi khó hiểu, nhưng thực tế là các tệp JavaScript, có thể xác minh được 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 tương ứng nếu bạn muốn). Bây giờ là lúc kiểm thử đầu tiên để xem liệu mã có hoạt động 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 nó hoạt động trong trình duyệt.

mkbitmap bằng WebAssembly trong trình duyệt

Sao chép các tệp mkbitmap.jsmkbitmap.wasm vào thư mục mới có tên mkbitmap và tạo một tệp nguyên mẫu HTML index.html 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 của bạn. Bạn sẽ thấy một lời nhắc yêu cầu bạn nhập thông tin. Điều này đúng như dự kiến, vì theo trang man của công cụ, "[i]f không có đối số tên tệp nào được đưa ra, thì mkbitmap đóng vai trò là bộ lọc, đọc từ dữ liệu đầu vào chuẩn", mà theo mặc định cho Emscripten là prompt().

Ứng dụng mkbitmap cho thấy lời nhắc yêu cầu nhập thông tin.

Ngăn thực thi tự động

Để ngăn mkbitmap thực thi ngay lập tức và thay vào đó làm cho nó chờ hoạt động đầu vào của người dùng, bạn cần hiểu rõ đối tượng Module của Emscripten. Module là một đối tượng JavaScript toàn cầu có các thuộc tính mà lệnh gọi mã do Emscripten tạo tại nhiều thời điểm trong quá trình thực thi. Bạn có thể cung cấp phương thức triển khai Module để kiểm soát việc thực thi mã. Khi khởi động, ứng dụng Emscripten 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 đầu tiên làm cho lời nhắc xuất hiện. Tạo một tập lệnh có tên là script.js, đưa tập lệnh đó vào trước <script src="mkbitmap.js"></script> trong index.html rồi thêm đoạn mã sau vào script.js. Bây giờ khi 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 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 dịch vụ hỗ trợ hệ thống tệp của Emscripten trong Module.FS. Phần Bao gồm dịch vụ hỗ trợ hệ thống tệp trong tài liệu nêu rõ:

Emscripten sẽ quyết định xem có tự động hỗ trợ hệ thống tệp hay không. Nhiều chương trình không cần tệp và tính năng hỗ trợ hệ thống tệp không đáng kể, vì vậy, Emscripten sẽ tránh đưa chương trình này vào khi không tìm thấy lý do. Điều đó có nghĩa là nếu mã C/C++ của bạn không truy cập vào 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 kết quả. Mặt khác, nếu mã C/C++ của bạn sử dụng tệp thì dịch vụ 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 Emscripten không tự động hỗ trợ hệ thống tệp, vì vậy bạn cần phải chỉ rõ việc này là như vậy. Tức là bạn cần làm theo các bước emconfigureemmake được 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.

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 mà 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'

Hãy nhớ chạy lại emmake make và sao chép các tệp mới tạo vào thư mục mkbitmap.

Sửa đổi index.html để chỉ tải mô-đun ES script.js mà từ đó bạn nhập mô-đun mkbitmap.js.

<!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 lại vào bảng điều khiển Công cụ cho nhà phát triển và lời nhắc sẽ biến mất vì hàm main() của mkbitmap không còn được gọi lúc bắt đầu.

Ứng dụng mkbitmap có màn hình màu trắng, cho thấy đối tượng Mô-đun được ghi vào bảng điều khiển Công cụ cho nhà phát triển.

Thực thi hàm chính theo cách thủ công

Bước tiếp theo là gọi hàm main() của mkbitmap theo cách thủ công bằng cách chạy Module.callMain(). Hàm callMain() nhận một mảng các đối số khớp với từng đối số mà bạn sẽ truyền vào 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 lại 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();

Ứng dụng mkbitmap có màn hình màu trắng, cho thấy số phiên bản mkbitmap được ghi vào bảng điều khiển Công cụ cho nhà phát triển.

Chuyển hướng đầu ra chuẩn

Theo mặc định, đầu ra chuẩn (stdout) là bảng điều khiển. Tuy nhiên, bạn có thể chuyển hướng đến một hàm khác, chẳng hạn như một hàm lưu trữ dữ liệu đầu ra cho một biến. Tức là bạn có thể thêm dữ liệu đầ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();

Ứng dụng mkbitmap cho thấy số phiên bản mkbitmap.

Đư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 bộ nhớ, bạn cần có mã 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, hãy tìm hiểu một số thông tin cơ bản về cách mkbitmap dự kiến đầu vào và tạo đầu ra.

Các định dạng đầu vào được hỗ trợ của mkbitmapPNM (PBM, PGM, PPM) và BMP. Các định dạng đầu ra là PBM đối với bitmap và PGM đối với bản đồ màu xám. Nếu bạn cung cấp một đối số filename, thì theo mặc định, mkbitmap sẽ tạo một tệp đầu ra có tên 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ộ để có thể biên dịch và chạy mã gốc sử dụng các API tệp đồng bộ mà không cần thay đổi hoặc ít thay đổi. Để mkbitmap đọc 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 một hệ thống tệp trong bộ nhớ hỗ trợ (thường được gọi là MEMFS) và có 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 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 với 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á. Lý do thực tế là Module.callMain() là một hàm thường chỉ được 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();

Ứng dụng mkbitmap cho thấy một mảng các tệp trong hệ thống tệp bộ nhớ, bao gồm cả example.bmp.

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 '/' của 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();

Ứng dụng mkbitmap cho thấy một mảng các tệp trong hệ thống tệp bộ nhớ, bao gồm example.bmp và example.pbm.

Lấy tệp đầu ra từ hệ thống tệp bộ nhớ

Hàm readFile() của đối tượng FS cho phép tạo example.pbm ở bước cuối cùng khỏi hệ thống tệp 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 tinh tế hơn để lưu tệp, nhưng sử dụng <a download> được tạo linh độ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 hình ảnh yêu thích của mình.

// 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();

Trình tìm kiếm macOS với bản xem trước tệp .bmp đầu vào và tệp .pbm đầu ra.

Thêm giao diện người dùng tương tác

Cho đến thời điểm này, tệp đầu vào được mã hoá cứng và mkbitmap chạy bằng các tham số mặc định. Bước cuối cùng là để người dùng linh động chọn một tệp đầu vào, điều chỉnh các tham số mkbitmap, rồi chạy công cụ này với các tuỳ 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 đặc biệt khó phân tích, vì vậy với một số mã JavaScript, bạn thậm chí có thể hiển thị 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 lên WebAssembly và khiến nó hoạt động trong trình duyệt! Có một số ngõ cụt và bạn phải biên dịch công cụ này nhiều lần cho đến khi nó hoạt động, nhưng như tôi đã viết ở trên, đó là một phần của trải nghiệm đó. Ngoài ra, hãy ghi nhớ 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!

Xác nhận

Bài viết này đã được Sam CleggRachel Andrew xem xét.