Các API I/O trên web không đồng bộ nhưng đồng bộ trong hầu hết các ngôn ngữ hệ thống. Khi biên dịch mã thành WebAssembly, bạn cần kết nối một loại API với một loại API khác và cầu nối này là Asyncify. Trong bài đăng này, bạn sẽ tìm hiểu thời điểm và cách sử dụng Asyncify cũng như cách hoạt động của công cụ này.
I/O bằng ngôn ngữ hệ thống
Tôi sẽ bắt đầu với một ví dụ đơn giản trong ngôn ngữ C. Giả sử bạn muốn đọc tên của người dùng trong tệp và chào họ bằng thông báo "Hello, (username)!" (Xin chào, (tên người dùng)!):
#include <stdio.h>
int main() {
FILE *stream = fopen("name.txt", "r");
char name[20+1];
size_t len = fread(&name, 1, 20, stream);
name[len] = '\0';
fclose(stream);
printf("Hello, %s!\n", name);
return 0;
}
Mặc dù ví dụ này không làm được nhiều việc, nhưng nó đã minh hoạ một số điều bạn sẽ thấy trong một ứng dụng bất kỳ: ứng dụng này đọc một số dữ liệu đầu vào từ thế giới bên ngoài, xử lý các dữ liệu đó trong nội bộ và ghi dữ liệu đầu ra trở lại thế giới bên ngoài. Tất cả các hoạt động tương tác như vậy với thế giới bên ngoài đều diễn ra thông qua một số hàm thường được gọi là hàm đầu vào-đầu ra, còn gọi tắt là I/O.
Để đọc tên từ C, bạn cần ít nhất hai lệnh gọi I/O quan trọng: fopen
để mở tệp và fread
để đọc dữ liệu từ tệp đó. Sau khi truy xuất dữ liệu, bạn có thể sử dụng một hàm I/O khác printf
để in kết quả ra bảng điều khiển.
Các hàm đó thoạt nhìn có vẻ khá đơn giản và bạn không phải suy nghĩ kỹ về máy móc liên quan để đọc hoặc ghi dữ liệu. Tuy nhiên, tuỳ thuộc vào môi trường, có thể có khá nhiều điều xảy ra bên trong:
- Nếu tệp đầu vào nằm trên ổ cục bộ, thì ứng dụng cần thực hiện một loạt thao tác truy cập vào bộ nhớ và ổ đĩa để xác định vị trí tệp, kiểm tra quyền, mở tệp để đọc, sau đó đọc từng khối cho đến khi truy xuất được số byte được yêu cầu. Quá trình này có thể khá chậm, tuỳ thuộc vào tốc độ của ổ đĩa và kích thước được yêu cầu.
- Hoặc tệp đầu vào có thể nằm trên một vị trí mạng được gắn, trong trường hợp này, ngăn xếp mạng cũng sẽ tham gia, làm tăng độ phức tạp, độ trễ và số lần thử lại tiềm năng cho mỗi thao tác.
- Cuối cùng, ngay cả
printf
cũng không được đảm bảo sẽ in nội dung vào bảng điều khiển và có thể được chuyển hướng đến một tệp hoặc vị trí mạng. Trong trường hợp này,printf
sẽ phải thực hiện các bước tương tự như trên.
Tóm lại, I/O có thể bị chậm và bạn không thể dự đoán thời lượng của một lệnh gọi cụ thể bằng cách xem nhanh mã. Trong khi thao tác đó đang chạy, toàn bộ ứng dụng của bạn sẽ có vẻ như bị treo và không phản hồi người dùng.
Điều này không chỉ giới hạn ở C hoặc C++. Hầu hết ngôn ngữ hệ thống hiển thị tất cả I/O dưới dạng API đồng bộ. Ví dụ: nếu bạn dịch ví dụ này sang Rust, API có thể trông đơn giản hơn, nhưng các nguyên tắc tương tự vẫn áp dụng. Bạn chỉ cần thực hiện một lệnh gọi và đồng bộ chờ lệnh gọi đó trả về kết quả, trong khi lệnh gọi đó thực hiện tất cả các thao tác tốn kém và cuối cùng trả về kết quả trong một lệnh gọi duy nhất:
fn main() {
let s = std::fs::read_to_string("name.txt");
println!("Hello, {}!", s);
}
Nhưng điều gì sẽ xảy ra khi bạn cố gắng biên dịch bất kỳ mẫu nào trong số đó sang WebAssembly và dịch chúng lên web? Hoặc, để đưa ra một ví dụ cụ thể, thao tác "đọc tệp" có thể dịch thành gì? Ứng dụng này cần đọc dữ liệu từ một số bộ nhớ.
Mô hình không đồng bộ của web
Web có nhiều lựa chọn bộ nhớ khác nhau mà bạn có thể liên kết đến, chẳng hạn như bộ nhớ trong bộ nhớ (đối tượng JS), localStorage
, IndexedDB, bộ nhớ phía máy chủ và API truy cập hệ thống tệp mới.
Tuy nhiên, bạn chỉ có thể sử dụng đồng bộ hai trong số các API đó (bộ nhớ trong và localStorage
). Cả hai đều là các tuỳ chọn hạn chế nhất về nội dung bạn có thể lưu trữ và thời lượng lưu trữ. Tất cả các tuỳ chọn khác chỉ cung cấp API không đồng bộ.
Đây là một trong những thuộc tính cốt lõi của việc thực thi mã trên web: mọi thao tác tốn thời gian, bao gồm cả mọi thao tác I/O, đều phải không đồng bộ.
Lý do là web trước đây là đơn luồng và mọi mã người dùng chạm vào giao diện người dùng đều phải chạy trên cùng một luồng với giao diện người dùng. Quá trình này phải cạnh tranh với các tác vụ quan trọng khác như bố cục, kết xuất và xử lý sự kiện để có thời gian CPU. Bạn không muốn một đoạn JavaScript hoặc WebAssembly có thể bắt đầu thao tác "đọc tệp" và chặn mọi thứ khác – toàn bộ thẻ hoặc trước đây là toàn bộ trình duyệt – trong khoảng từ mili giây đến vài giây cho đến khi thao tác này kết thúc.
Thay vào đó, mã chỉ được phép lên lịch một thao tác I/O cùng với lệnh gọi lại sẽ được thực thi sau khi hoàn tất. Các lệnh gọi lại như vậy được thực thi trong vòng lặp sự kiện của trình duyệt. Tôi sẽ không đi sâu vào chi tiết ở đây, nhưng nếu bạn muốn tìm hiểu cách hoạt động của vòng lặp sự kiện, hãy xem bài viết Tác vụ, tác vụ vi mô, hàng đợi và lịch biểu để hiểu rõ hơn về chủ đề này.
Phiên bản ngắn gọn là trình duyệt chạy tất cả các đoạn mã theo dạng một vòng lặp vô hạn, bằng cách lấy từng đoạn mã ra khỏi hàng đợi. Khi một sự kiện nào đó được kích hoạt, trình duyệt sẽ đưa trình xử lý tương ứng vào hàng đợi và trong vòng lặp lặp lại tiếp theo, trình xử lý đó sẽ được lấy ra khỏi hàng đợi và thực thi. Cơ chế này cho phép mô phỏng tính năng đồng thời và chạy nhiều thao tác song song trong khi chỉ sử dụng một luồng.
Điều quan trọng cần nhớ về cơ chế này là trong khi mã JavaScript (hoặc WebAssembly) tuỳ chỉnh của bạn thực thi, vòng lặp sự kiện sẽ bị chặn và trong khi đó, không có cách nào để phản ứng với bất kỳ trình xử lý, sự kiện, I/O bên ngoài nào, v.v. Cách duy nhất để lấy lại kết quả I/O là đăng ký lệnh gọi lại, hoàn tất việc thực thi mã và trả lại quyền kiểm soát cho trình duyệt để trình duyệt có thể tiếp tục xử lý mọi tác vụ đang chờ xử lý. Sau khi I/O hoàn tất, trình xử lý của bạn sẽ trở thành một trong những tác vụ đó và sẽ được thực thi.
Ví dụ: nếu muốn viết lại các mẫu ở trên bằng JavaScript hiện đại và quyết định đọc tên từ một URL từ xa, bạn sẽ sử dụng Fetch API và cú pháp async-await:
async function main() {
let response = await fetch("name.txt");
let name = await response.text();
console.log("Hello, %s!", name);
}
Mặc dù có vẻ như đồng bộ, nhưng về cơ bản, mỗi await
về cơ bản là cú pháp đơn giản cho lệnh gọi lại:
function main() {
return fetch("name.txt")
.then(response => response.text())
.then(name => console.log("Hello, %s!", name));
}
Trong ví dụ đơn giản hơn này, một yêu cầu được bắt đầu và các phản hồi được đăng ký bằng lệnh gọi lại đầu tiên. Sau khi nhận được phản hồi ban đầu (chỉ là tiêu đề HTTP), trình duyệt sẽ gọi lại không đồng bộ lệnh gọi lại này. Lệnh gọi lại bắt đầu đọc nội dung dưới dạng văn bản bằng cách sử dụng response.text()
và đăng ký kết quả bằng một lệnh gọi lại khác. Cuối cùng, sau khi fetch
truy xuất tất cả nội dung, hàm này sẽ gọi lệnh gọi lại cuối cùng để in "Xin chào, (tên người dùng)!" vào bảng điều khiển.
Do tính chất không đồng bộ của các bước đó nên hàm gốc có thể trả về quyền kiểm soát cho trình duyệt ngay khi I/O đã được lên lịch và để lại toàn bộ giao diện người dùng thích ứng cũng như thực hiện các tác vụ khác, bao gồm cả hiển thị, cuộn, v.v. trong khi I/O đang thực thi ở chế độ nền.
Cuối cùng, ngay cả các API đơn giản như "ngủ" (yêu cầu ứng dụng đợi một số giây được chỉ định) cũng là một dạng của thao tác I/O:
#include <stdio.h>
#include <unistd.h>
// ...
printf("A\n");
sleep(1);
printf("B\n");
Chắc chắn, bạn có thể dịch hàm này theo cách rất đơn giản để chặn luồng hiện tại cho đến khi hết thời gian:
console.log("A");
for (let start = Date.now(); Date.now() - start < 1000;);
console.log("B");
Trên thực tế, đó chính xác là những gì Emscripten thực hiện trong quy trình triển khai mặc định của "sleep", nhưng điều đó rất không hiệu quả, sẽ chặn toàn bộ giao diện người dùng và không cho phép xử lý bất kỳ sự kiện nào khác trong khi đó. Nói chung, đừng làm như vậy trong mã phát hành.
Thay vào đó, phiên bản "ngủ" trong JavaScript phù hợp hơn sẽ liên quan đến việc gọi setTimeout()
và đăng ký bằng một trình xử lý:
console.log("A");
setTimeout(() => {
console.log("B");
}, 1000);
Điểm chung của tất cả các ví dụ và API này là gì? Trong mỗi trường hợp, mã theo ngôn ngữ gốc của hệ thống sử dụng API chặn cho I/O, trong khi ví dụ tương đương cho web sử dụng API không đồng bộ. Khi biên dịch cho web, bạn cần chuyển đổi giữa hai mô hình thực thi đó theo cách nào đó và WebAssembly chưa có khả năng tích hợp để thực hiện việc này.
Cầu nối với Asyncify
Đây là lúc Asyncify phát huy tác dụng. Asyncify là một tính năng thời gian biên dịch do Emscripten hỗ trợ, cho phép tạm dừng toàn bộ chương trình và tiếp tục không đồng bộ sau đó.
Cách sử dụng trong C/C++ với Emscripten
Nếu muốn sử dụng Asyncify để triển khai chế độ ngủ không đồng bộ cho ví dụ cuối cùng, bạn có thể làm như sau:
#include <stdio.h>
#include <emscripten.h>
EM_JS(void, async_sleep, (int seconds), {
Asyncify.handleSleep(wakeUp => {
setTimeout(wakeUp, seconds * 1000);
});
});
…
puts("A");
async_sleep(1);
puts("B");
EM_JS
là một macro cho phép xác định các đoạn mã JavaScript như thể chúng là hàm C. Bên trong, hãy sử dụng hàm Asyncify.handleSleep()
để yêu cầu Emscripten tạm ngưng chương trình và cung cấp trình xử lý wakeUp()
sẽ được gọi sau khi thao tác không đồng bộ hoàn tất. Trong ví dụ trên, trình xử lý được truyền đến setTimeout()
, nhưng bạn có thể sử dụng trình xử lý này trong bất kỳ ngữ cảnh nào khác chấp nhận lệnh gọi lại. Cuối cùng, bạn có thể gọi async_sleep()
ở bất cứ đâu bạn muốn, giống như sleep()
thông thường hoặc bất kỳ API đồng bộ nào khác.
Khi biên dịch mã như vậy, bạn cần yêu cầu Emscripten kích hoạt tính năng Asyncify. Bạn có thể thực hiện việc này bằng cách truyền -s ASYNCIFY
cũng như -s ASYNCIFY_IMPORTS=[func1,
func2]
với một danh sách hàm có dạng mảng có thể không đồng bộ.
emcc -O2 \
-s ASYNCIFY \
-s ASYNCIFY_IMPORTS=[async_sleep] \
...
Điều này cho phép Emscripten biết rằng mọi lệnh gọi đến các hàm đó có thể yêu cầu lưu và khôi phục trạng thái, vì vậy, trình biên dịch sẽ chèn mã hỗ trợ xung quanh các lệnh gọi đó.
Bây giờ, khi thực thi mã này trong trình duyệt, bạn sẽ thấy một nhật ký đầu ra liền mạch như mong đợi, trong đó B xuất hiện sau một khoảng thời gian trễ ngắn sau A.
A
B
Bạn cũng có thể trả về giá trị từ các hàm Asyncify. Bạn cần trả về kết quả của handleSleep()
và truyền kết quả đó đến lệnh gọi lại wakeUp()
. Ví dụ: nếu thay vì đọc từ tệp, bạn muốn tìm nạp một số từ tài nguyên từ xa, bạn có thể sử dụng một đoạn mã như đoạn mã dưới đây để đưa ra yêu cầu, tạm ngưng mã C và tiếp tục sau khi truy xuất nội dung phản hồi – tất cả đều được thực hiện liền mạch như thể lệnh gọi là đồng bộ.
EM_JS(int, get_answer, (), {
return Asyncify.handleSleep(wakeUp => {
fetch("answer.txt")
.then(response => response.text())
.then(text => wakeUp(Number(text)));
});
});
puts("Getting answer...");
int answer = get_answer();
printf("Answer is %d\n", answer);
Trên thực tế, đối với các API dựa trên Lời hứa như fetch()
, bạn thậm chí có thể kết hợp Asyncify với tính năng async-await của JavaScript thay vì sử dụng API dựa trên lệnh gọi lại. Để làm việc đó, thay vì Asyncify.handleSleep()
, hãy gọi Asyncify.handleAsync()
. Sau đó, thay vì phải lên lịch gọi lại wakeUp()
, bạn có thể truyền một hàm JavaScript async
và sử dụng await
và return
bên trong, giúp mã trông tự nhiên và đồng bộ hơn, đồng thời không mất bất kỳ lợi ích nào của I/O không đồng bộ.
EM_JS(int, get_answer, (), {
return Asyncify.handleAsync(async () => {
let response = await fetch("answer.txt");
let text = await response.text();
return Number(text);
});
});
int answer = get_answer();
Đang chờ giá trị phức tạp
Tuy nhiên, ví dụ này vẫn chỉ giới hạn ở các số. Nếu bạn muốn triển khai ví dụ ban đầu, trong đó tôi đã cố gắng lấy tên người dùng từ một tệp dưới dạng chuỗi thì sao? Bạn cũng có thể làm như vậy!
Emscripten cung cấp một tính năng có tên là Embind cho phép bạn xử lý việc chuyển đổi giữa các giá trị JavaScript và C++. Thư viện này cũng hỗ trợ Asyncify, vì vậy, bạn có thể gọi await()
trên Promise
bên ngoài và thư viện này sẽ hoạt động giống như await
trong mã JavaScript không đồng bộ-chờ:
val fetch = val::global("fetch");
val response = fetch(std::string("answer.txt")).await();
val text = response.call<val>("text").await();
auto answer = text.as<std::string>();
Khi sử dụng phương thức này, bạn thậm chí không cần truyền ASYNCIFY_IMPORTS
dưới dạng cờ biên dịch, vì cờ này đã được đưa vào theo mặc định.
Được rồi, tất cả đều hoạt động tốt trong Emscripten. Còn các ngôn ngữ và chuỗi công cụ khác thì sao?
Cách sử dụng từ các ngôn ngữ khác
Giả sử bạn có một lệnh gọi đồng bộ tương tự ở đâu đó trong mã Rust mà bạn muốn ánh xạ đến một API không đồng bộ trên web. Hóa ra, bạn cũng có thể làm được điều đó!
Trước tiên, bạn cần xác định một hàm như vậy là một hàm nhập thông thường thông qua khối extern
(hoặc cú pháp của ngôn ngữ bạn chọn cho các hàm ngoại).
extern {
fn get_answer() -> i32;
}
println!("Getting answer...");
let answer = get_answer();
println!("Answer is {}", answer);
Và biên dịch mã của bạn thành WebAssembly:
cargo build --target wasm32-unknown-unknown
Bây giờ, bạn cần đo lường tệp WebAssembly bằng mã để lưu trữ/khôi phục ngăn xếp. Đối với C/C++, Emscripten sẽ làm việc này cho chúng ta, nhưng nó không được dùng ở đây, do đó, quy trình này thủ công hơn một chút.
May mắn thay, bản thân phép biến đổi Asyncify hoàn toàn không phụ thuộc vào chuỗi công cụ. Giao thức này có thể biến đổi các tệp WebAssembly tuỳ ý, bất kể trình biên dịch tạo tệp đó là gì. Biến đổi được cung cấp riêng
trong trình tối ưu hoá wasm-opt
từ chuỗi công cụ
Binaryen và có thể được gọi như sau:
wasm-opt -O2 --asyncify \
--pass-arg=asyncify-imports@env.get_answer \
[...]
Truyền --asyncify
để bật tính năng biến đổi, sau đó sử dụng --pass-arg=…
để cung cấp danh sách các hàm không đồng bộ được phân tách bằng dấu phẩy, trong đó trạng thái chương trình sẽ bị tạm ngưng rồi sau đó được tiếp tục.
Tất cả những gì còn lại là cung cấp mã thời gian chạy hỗ trợ thực sự thực hiện việc đó – tạm ngưng và tiếp tục mã WebAssembly. Xin nhắc lại, trong trường hợp C/C++, Emscripten sẽ đưa mã này vào, nhưng giờ đây, bạn cần có mã kết nối JavaScript tuỳ chỉnh để xử lý các tệp WebAssembly tuỳ ý. Chúng tôi đã tạo một thư viện để dành riêng cho điều đó.
Bạn có thể tìm thấy thư viện này trên GitHub tại https://github.com/GoogleChromeLabs/asyncify hoặc npm với tên asyncify-wasm
.
API này mô phỏng một API tạo bản sao WebAssembly chuẩn, nhưng trong không gian tên riêng. Điểm khác biệt duy nhất là trong API WebAssembly thông thường, bạn chỉ có thể cung cấp các hàm đồng bộ dưới dạng dữ liệu nhập, còn trong trình bao bọc Asyncify, bạn cũng có thể cung cấp các lệnh nhập không đồng bộ:
const { instance } = await Asyncify.instantiateStreaming(fetch('app.wasm'), {
env: {
async get_answer() {
let response = await fetch("answer.txt");
let text = await response.text();
return Number(text);
}
}
});
…
await instance.exports.main();
Sau khi bạn cố gắng gọi một hàm không đồng bộ như vậy – chẳng hạn như get_answer()
trong ví dụ ở trên – từ phía WebAssembly, thư viện sẽ phát hiện Promise
được trả về, tạm ngưng và lưu trạng thái của ứng dụng WebAssembly, đăng ký hoàn tất lời hứa và sau đó, khi lời hứa được giải quyết, thư viện sẽ khôi phục liền mạch ngăn xếp lệnh gọi và trạng thái, đồng thời tiếp tục thực thi như thể không có gì xảy ra.
Vì mọi hàm trong mô-đun đều có thể thực hiện lệnh gọi không đồng bộ, nên tất cả các lệnh xuất cũng có thể trở thành không đồng bộ, do đó, các lệnh xuất này cũng được gói lại. Bạn có thể nhận thấy trong ví dụ trên rằng bạn cần await
kết quả của instance.exports.main()
để biết thời điểm thực thi thực sự kết thúc.
Tất cả những điều này hoạt động như thế nào?
Khi phát hiện một lệnh gọi đến một trong các hàm ASYNCIFY_IMPORTS
, Asyncify sẽ bắt đầu một thao tác không đồng bộ, lưu toàn bộ trạng thái của ứng dụng, bao gồm cả ngăn xếp lệnh gọi và mọi biến cục bộ tạm thời, sau đó, khi thao tác đó hoàn tất, khôi phục tất cả bộ nhớ và ngăn xếp lệnh gọi, đồng thời tiếp tục từ cùng một vị trí và với cùng trạng thái như thể chương trình chưa bao giờ dừng.
Điều này khá giống với tính năng async-await trong JavaScript mà tôi đã trình bày trước đó, nhưng không giống như tính năng trong JavaScript, tính năng này không yêu cầu bất kỳ cú pháp đặc biệt hoặc hỗ trợ thời gian chạy nào từ ngôn ngữ, mà thay vào đó, tính năng này hoạt động bằng cách chuyển đổi các hàm đồng bộ thuần tuý tại thời điểm biên dịch.
Khi biên dịch ví dụ về trạng thái ngủ không đồng bộ đã trình bày trước đó:
puts("A");
async_sleep(1);
puts("B");
Asyncify sẽ lấy mã này và chuyển đổi mã đó thành mã tương tự như mã sau (mã giả lập, quá trình chuyển đổi thực tế sẽ phức tạp hơn):
if (mode == NORMAL_EXECUTION) {
puts("A");
async_sleep(1);
saveLocals();
mode = UNWINDING;
return;
}
if (mode == REWINDING) {
restoreLocals();
mode = NORMAL_EXECUTION;
}
puts("B");
Ban đầu, mode
được đặt thành NORMAL_EXECUTION
. Tương ứng, trong lần đầu tiên mã đã chuyển đổi như vậy được thực thi, chỉ phần dẫn đến async_sleep()
mới được đánh giá. Ngay sau khi hoạt động không đồng bộ được lên lịch, tính năng Asyncify sẽ lưu tất cả các hàm cục bộ và gỡ bỏ ngăn xếp bằng cách quay lại từ mỗi hàm lên trên cùng, cách này sẽ giúp trả lại quyền kiểm soát cho vòng lặp sự kiện của trình duyệt.
Sau đó, khi async_sleep()
phân giải, mã hỗ trợ Asyncify sẽ thay đổi mode
thành REWINDING
và gọi lại hàm. Lần này, nhánh "thực thi thông thường" bị bỏ qua – vì nó đã thực hiện
công việc lần trước và tôi muốn tránh in "A" hai lần, và thay vào đó, nhánh này sẽ chuyển thẳng đến nhánh "tua lại". Sau khi đạt đến, mã này sẽ khôi phục tất cả dữ liệu cục bộ được lưu trữ, thay đổi chế độ về "bình thường" và tiếp tục quá trình thực thi như thể mã chưa từng bị dừng ngay từ đầu.
Chi phí chuyển đổi
Rất tiếc, việc chuyển đổi Asyncify không hoàn toàn miễn phí, vì nó phải chèn khá nhiều mã hỗ trợ để lưu trữ và khôi phục tất cả các biến cục bộ đó, điều hướng ngăn xếp lệnh gọi ở nhiều chế độ, v.v. Công cụ này cố gắng chỉ sửa đổi các hàm được đánh dấu là không đồng bộ trên dòng lệnh, cũng như mọi phương thức gọi tiềm năng của các hàm đó, nhưng mức hao tổn kích thước mã vẫn có thể tăng thêm khoảng 50% trước khi nén.
Đây không phải là giải pháp lý tưởng, nhưng trong nhiều trường hợp, bạn có thể chấp nhận khi giải pháp thay thế là không có chức năng này hoặc phải viết lại đáng kể mã gốc.
Hãy nhớ luôn bật tính năng tối ưu hoá cho các bản dựng cuối cùng để tránh tình trạng này trở nên nghiêm trọng hơn. Bạn cũng có thể kiểm tra các tuỳ chọn tối ưu hoá dành riêng cho Asyncify để giảm hao tổn bằng cách chỉ giới hạn các phép biến đổi ở các hàm được chỉ định và/hoặc chỉ các lệnh gọi hàm trực tiếp. Hiệu suất thời gian chạy cũng có một chút hao tổn, nhưng chỉ giới hạn ở các lệnh gọi không đồng bộ. Tuy nhiên, so với chi phí thực tế của công việc, chi phí này thường không đáng kể.
Bản minh hoạ thực tế
Giờ đây, khi bạn đã xem các ví dụ đơn giản, tôi sẽ chuyển sang các tình huống phức tạp hơn.
Như đã đề cập ở đầu bài viết, một trong các tuỳ chọn bộ nhớ trên web là API Truy cập hệ thống tệp không đồng bộ. Thư viện này cung cấp quyền truy cập vào hệ thống tệp máy chủ thực từ một ứng dụng web.
Mặt khác, có một tiêu chuẩn thực tế được gọi là WASI cho I/O WebAssembly trong bảng điều khiển và phía máy chủ. Thư viện này được thiết kế làm mục tiêu biên dịch cho các ngôn ngữ hệ thống và hiển thị tất cả các loại hệ thống tệp cũng như các thao tác khác ở dạng đồng bộ truyền thống.
Nếu bạn có thể liên kết một lớp với một lớp khác thì sao? Sau đó, bạn có thể biên dịch bất kỳ ứng dụng nào bằng bất kỳ ngôn ngữ nguồn nào với bất kỳ chuỗi công cụ nào hỗ trợ mục tiêu WASI và chạy ứng dụng đó trong hộp cát trên web, đồng thời vẫn cho phép ứng dụng hoạt động trên các tệp người dùng thực! Với Asyncify, bạn có thể làm được điều đó.
Trong bản minh hoạ này, tôi đã biên dịch Rust coreutils với một số bản vá nhỏ cho WASI, được truyền qua phép biến đổi Asyncify và triển khai liên kết không đồng bộ từ WASI đến API Truy cập hệ thống tệp ở phía JavaScript. Sau khi kết hợp với thành phần đầu cuối Xterm.js, thành phần này sẽ cung cấp một màn hình shell thực tế chạy trong thẻ trình duyệt và hoạt động trên các tệp người dùng thực – giống như một màn hình đầu cuối thực tế.
Hãy xem trực tiếp tại https://wasi.rreverser.com/.
Các trường hợp sử dụng Asyncify không chỉ giới hạn ở bộ hẹn giờ và hệ thống tệp. Bạn có thể tiến xa hơn và sử dụng nhiều API chuyên biệt hơn trên web.
Ví dụ: cũng nhờ sự trợ giúp của Asyncify, bạn có thể liên kết libusb – có thể là thư viện gốc phổ biến nhất để làm việc với các thiết bị USB – với API WebUSB, cho phép truy cập không đồng bộ vào các thiết bị như vậy trên web. Sau khi ánh xạ và biên dịch, tôi đã có các ví dụ và kiểm thử libusb tiêu chuẩn để chạy trên các thiết bị đã chọn ngay trong hộp cát của một trang web.
Tuy nhiên, đó có thể là câu chuyện cho một bài đăng khác trên blog.
Những ví dụ đó cho thấy Asyncify có thể hiệu quả như thế nào trong việc thu hẹp khoảng cách và chuyển tất cả các loại ứng dụng sang web, cho phép bạn có quyền truy cập trên nhiều nền tảng, tạo hộp cát và bảo mật tốt hơn mà không làm mất chức năng.