Web'deki G/Ç API'leri asenkrondur ancak çoğu sistem dilinde senkrondur. Kodu WebAssembly'e derleyirken bir API türünü diğerine bağlamanız gerekir. Bu köprü Asyncify'dir. Bu yayında, Asyncify'yi ne zaman ve nasıl kullanacağınızı ve nasıl çalıştığını öğreneceksiniz.
Sistem dillerinde G/Ç
C dilinde basit bir örnekle başlayacağım. Örneğin, bir dosyadan kullanıcının adını okumak ve kullanıcıyı "Merhaba (kullanıcı adı)" mesajıyla karşılamak istiyorsunuz:
#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;
}
Bu örnek çok fazla bir işe yaramasa da her boyuttaki uygulamada göreceğiniz bir şeyi zaten gösterir. Dış dünyadan gelen bazı girişleri okur, bunları dahili olarak işler ve çıktıları dış dünyaya yazar. Dış dünyayla yapılan tüm bu etkileşimler, genellikle G/Ç olarak kısaltılan ve giriş/çıkış işlevleri olarak adlandırılan birkaç işlev aracılığıyla gerçekleşir.
C'den adı okumak için en az iki önemli G/Ç çağrısına ihtiyacınız vardır: Dosyayı açmak için fopen
ve dosyasındaki verileri okumak için fread
. Verileri aldıktan sonra sonucu konsola yazdırmak için başka bir G/Ç işlevi printf
kullanabilirsiniz.
Bu işlevler ilk bakışta oldukça basit görünür ve veri okumak veya yazmak için gereken mekanizmalar hakkında iki kez düşünmeniz gerekmez. Ancak ortama bağlı olarak, cihazın içinde oldukça fazla işlem gerçekleşebilir:
- Giriş dosyası yerel bir sürücüdeyse uygulamanın dosyayı bulmak, izinleri kontrol etmek, okumak için açmak ve ardından istenen bayt sayısı alıncaya kadar blok halinde okumak için bir dizi bellek ve disk erişimi gerçekleştirmesi gerekir. Bu işlem, diskinizin hızına ve istenen boyuta bağlı olarak oldukça yavaş olabilir.
- Veya giriş dosyası, eklenmiş bir ağ konumunda bulunuyor olabilir. Bu durumda, ağ yığını da artık dahil edilir ve her bir işlem için karmaşıklığı, gecikmeyi ve olası yeniden deneme sayısını artırır.
- Son olarak,
printf
komutunun bile konsola yazdıracağı garanti edilmez ve bir dosyaya veya ağ konumuna yönlendirilebilir. Bu durumda, yukarıdaki adımlardan aynı şekilde uygulanması gerekir.
Kısacası, G/Ç yavaş olabilir ve koda hızlıca göz atarak belirli bir çağrının ne kadar süreceğini tahmin edemezsiniz. Bu işlem çalışırken uygulamanızın tamamı donmuş ve kullanıcıya yanıt vermiyor olarak görünür.
Bu, C veya C++ ile de sınırlı değildir. Çoğu sistem dili, tüm G/Ç'yi senkronize API'ler biçiminde sunar. Örneğin, örneği Rust'a çevirirseniz API daha basit görünebilir ancak aynı ilkeler geçerlidir. Bir çağrı yapar ve sonucu döndürmesini eşzamanlı olarak beklersiniz. Bu sırada, pahalı işlemler gerçekleştirir ve sonucu tek bir çağrıda döndürür.
fn main() {
let s = std::fs::read_to_string("name.txt");
println!("Hello, {}!", s);
}
Ancak bu örneklerden herhangi birini WebAssembly'e derlemeye ve web'e çevirmeye çalıştığınızda ne olur? Belirli bir örnek vermek gerekirse "dosya okuma" işlemi ne anlama gelebilir? Bazı depolama alanlarından veri okuması gerekir.
Web'in eşzamansız modeli
Web'de eşleyebileceğiniz çeşitli depolama alanı seçenekleri vardır. Örneğin, bellek içi depolama (JS nesneleri), localStorage
, IndexedDB, sunucu tarafı depolama ve yeni Dosya Sistemi Erişimi API'si.
Ancak bu API'lerden yalnızca ikisi (bellek içi depolama ve localStorage
) eşzamanlı olarak kullanılabilir ve bunların her ikisi de depolayabileceğiniz öğeler ve süre konusunda en sınırlayıcı seçeneklerdir. Diğer tüm seçenekler yalnızca asenkron API'ler sağlar.
Bu, web'de kod yürütmenin temel özelliklerinden biridir: G/Ç işlemlerini içeren, zaman alan tüm işlemlerin asenkron olması gerekir.
Bunun nedeni, web'in geçmişte tek iş parçacıklı olması ve kullanıcı arayüzüne dokunan tüm kullanıcı kodlarının kullanıcı arayüzü ile aynı iş parçacığı üzerinde çalışmasının gerekmesidir. CPU zamanı için düzen, oluşturma ve etkinlik işleme gibi diğer önemli görevlerle rekabet etmesi gerekir. Bir JavaScript veya WebAssembly parçasının, "dosya okuma" işlemi başlatıp işlemin tamamlanması için milisaniyeler ila birkaç saniye arasında sekmenin tamamını veya geçmişte tarayıcının tamamını engellemesini istemezsiniz.
Bunun yerine, kodun yalnızca bir G/Ç işlemini ve işlem tamamlandığında yürütülecek bir geri çağırmayla birlikte programlamasına izin verilir. Bu tür geri çağırmalar, tarayıcının etkinlik döngüsü kapsamında yürütülür. Burada ayrıntılara girmeyeceğim ancak etkinlik döngüsünün işleyiş şeklini öğrenmek istiyorsanız bu konuyu ayrıntılı olarak açıklayan Görevler, mikro görevler, sıralamalar ve planlamalar başlıklı makaleyi inceleyin.
Kısacası, tarayıcı tüm kod parçalarını sıradan tek tek alarak sonsuz döngü içinde çalıştırır. Bir etkinlik tetiklendiğinde, tarayıcı karşılık gelen işleyiciyi sıraya alır ve bir sonraki döngü yinelemesinde sıradan çıkarılarak yürütülür. Bu mekanizma, yalnızca tek bir iş parçacığı kullanırken eşzamanlılık simüle edilmesine ve çok sayıda paralel işlem yürütülmesine olanak tanır.
Bu mekanizmayla ilgili olarak hatırlamanız gereken önemli nokta, özel JavaScript (veya WebAssembly) kodunuz yürütülürken etkinlik döngüsünün engellenmesidir. Engellenmiş durumdayken harici işleyicilere, etkinliklere, G/Ç'ye vb. tepki veremezsiniz. G/Ç sonuçlarını geri almanın tek yolu, bir geri çağırma işlevi kaydetmek, kodunuzu yürütmeyi tamamlamak ve bekleyen görevleri işlemeye devam edebilmesi için kontrolü tarayıcıya geri vermektir. G/Ç tamamlandığında, işleyiciniz bu görevlerden biri olur ve yürütülür.
Örneğin, yukarıdaki örnekleri modern JavaScript'te yeniden yazmak ve uzak bir URL'den bir ad okumaya karar vermek istiyorsanız Getirme API'sini ve eşzamansız-bekleme söz dizimini kullanırsınız:
async function main() {
let response = await fetch("name.txt");
let name = await response.text();
console.log("Hello, %s!", name);
}
Senkronize görünse de her await
, temelde geri çağırma işlevleri için söz dizimi şekeridir:
function main() {
return fetch("name.txt")
.then(response => response.text())
.then(name => console.log("Hello, %s!", name));
}
Bu biraz daha net olan şekersiz örnekte, bir istek başlatılır ve ilk geri çağırma ile yanıtlara abone olunur. Tarayıcı ilk yanıtı (yalnızca HTTP üstbilgileri) aldıktan sonra bu geri çağırma işlevini eşzamansız olarak çağırır. Geri çağırma, response.text()
kullanarak gövdeyi metin olarak okumaya başlar ve başka bir geri çağırma ile sonuca abone olur. Son olarak, fetch
tüm içerikleri aldıktan sonra, konsola "Hello, (username)!" yazan son geri çağırmayı çağırır.
Bu adımların eşzamansız doğası sayesinde orijinal işlev, G/Ç planlandığı anda kontrolü tarayıcıya döndürebilir ve G/Ç arka planda çalışırken, oluşturma, kaydırma vb. gibi diğer görevler için kullanıcı arayüzünün tamamını duyarlı ve kullanılabilir durumda bırakabilir.
Son örnek olarak, bir uygulamayı belirli bir süre bekleten "sleep" gibi basit API'ler de G/Ç işleminin bir biçimidir:
#include <stdio.h>
#include <unistd.h>
// ...
printf("A\n");
sleep(1);
printf("B\n");
Elbette, mevcut ileti dizisini süre dolana kadar engelleyecek çok basit bir şekilde çevirebilirsiniz:
console.log("A");
for (let start = Date.now(); Date.now() - start < 1000;);
console.log("B");
Hatta, Emscripten'in varsayılan "uyku" uygulamasında tam olarak bunu yapıyor, ancak bu çok verimsiz bir çözüm olan kullanıcı arayüzünün tamamını engeller ve bu esnada başka hiçbir etkinliğin işlenmesine izin vermez. Genellikle üretim kodunda bunu yapmayın.
Bunun yerine, JavaScript'te "sleep" işlevinin daha doğal bir versiyonu setTimeout()
işlevini çağırmayı ve bir işleyiciyle abone olmayı içerir:
console.log("A");
setTimeout(() => {
console.log("B");
}, 1000);
Tüm bu örneklerde ve API'lerde ortak olan nedir? Her iki durumda da, orijinal sistem dilinde bulunan yerel kodda G/Ç için engelleyen bir API kullanılırken web için eşdeğer bir örnekte bunun yerine asenkron bir API kullanılır. Web'e derleme yaparken bu iki yürütme modeli arasında bir şekilde dönüşüm yapmanız gerekir. WebAssembly'de henüz bunu yapabilen yerleşik bir özellik yoktur.
Asyncify ile boşluğu doldurma
İşte bu noktada Asyncify devreye girer. Asyncify, Emscripten tarafından desteklenen ve programın tamamını duraklatıp daha sonra ayarsız olarak devam ettirmenize olanak tanıyan bir derleme zamanı özelliğidir.
Emscripten ile C / C++'ta kullanım
Son örnekte zaman uyumsuz bir uyku uygulamak için Asyncify'i kullanmak isterseniz bunu aşağıdaki gibi yapabilirsiniz:
#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
, JavaScript snippet'lerinin C işlevleri gibi tanımlanmasına olanak tanıyan bir makrodur. İçinde, Emscripten'e programı askıya almasını söyleyen ve asenkron işlem tamamlandıktan sonra çağrılması gereken bir wakeUp()
işleyici sağlayan bir Asyncify.handleSleep()
işlevi kullanın. Yukarıdaki örnekte işleyici setTimeout()
'e iletiliyor ancak geri çağırma işlevlerini kabul eden diğer tüm bağlamlarda kullanılabilir. Son olarak, normal sleep()
veya diğer herhangi bir eşzamanlı API gibi istediğiniz yerde async_sleep()
öğesini çağırabilirsiniz.
Bu tür kodları derleyirken Emscripten'e Asyncify özelliğini etkinleştirmesini söylemeniz gerekir. Bunu yapmak için -s ASYNCIFY
ve -s ASYNCIFY_IMPORTS=[func1,
func2]
ile birlikte eşzamansız olabilecek işlevlerin dizi benzeri bir listesini iletin.
emcc -O2 \
-s ASYNCIFY \
-s ASYNCIFY_IMPORTS=[async_sleep] \
...
Böylece Emscripten, bu işlevlere yapılan çağrıların durumun kaydedilmesini ve geri yüklenmesini gerektirebilir. Bu sayede derleyici, bu tür çağrıların etrafına destekleyici kod ekler.
Şimdi bu kodu tarayıcıda çalıştırdığınızda, beklediğiniz gibi sorunsuz bir çıkış günlüğü görürsünüz. B, A'dan kısa bir süre sonra gelir.
A
B
Asyncify işlevlerinden de değer döndürebilirsiniz. Yapmanız gereken, handleSleep()
sonucunu döndürmek ve sonucu wakeUp()
geri çağırma işlevine iletmektir. Örneğin, dosyadan okumak yerine uzak bir kaynaktan numara getirmek isterseniz aşağıdaki gibi bir snippet'i kullanarak istek gönderebilir, C kodunu askıya alabilir ve yanıt gövdesi alındıktan sonra devam ettirebilirsiniz. Üstelik tüm bunlar, çağrı eşzamanlıymış gibi sorunsuz bir şekilde gerçekleşir.
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);
Hatta fetch()
gibi Promise tabanlı API'ler için geri çağırma tabanlı API'yi kullanmak yerine Asyncify'yi JavaScript'in async-await özelliğiyle birleştirebilirsiniz. Bunun için Asyncify.handleSleep()
yerine Asyncify.handleAsync()
numaralı telefonu arayın. Böylece, bir wakeUp()
geri çağırması planlamak yerine bir async
JavaScript işlevi iletebilir ve await
ile return
'i içeride kullanabilirsiniz. Böylece, kodun daha doğal ve eşzamanlı görünmesini sağlarken eşzamansız G/Ç'nin avantajlarından da yararlanabilirsiniz.
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();
Karmaşık değerler bekleniyor
Ancak bu örnekte yine de yalnızca sayılarla sınırlısınız. Bir dosyada kullanıcı adını dize olarak almaya çalıştığım orijinal örneği uygulamak isterseniz ne olur? Bunu da yapabilirsiniz.
Emscripten, JavaScript ile C++ değerleri arasında dönüşümleri yönetmenize olanak tanıyan Embind adlı bir özellik sağlar. Asyncify için de destek sunar. Böylece harici Promise
'larda await()
'ü çağırabilirsiniz. Bu işlev, async-await JavaScript kodundaki await
gibi çalışır:
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>();
Bu yöntemi kullanırken ASYNCIFY_IMPORTS
zaten varsayılan olarak dahil edildiği için derleme işareti olarak iletmeniz bile gerekmez.
Tamam, tüm bunlar Emscripten'de harika çalışıyor. Diğer araç zincirleri ve diller için ne yapabilirim?
Diğer dillerdeki kullanım
Rust kodunuzda, web'deki bir asenkron API ile eşlemek istediğiniz benzer bir senkron çağrınız olduğunu varsayalım. Bunu da yapabilirsiniz.
Öncelikle, bu tür bir işlevi extern
bloğu (veya seçtiğiniz dilin yabancı işlevler için söz dizimi) aracılığıyla normal bir içe aktarma olarak tanımlamanız gerekir.
extern {
fn get_answer() -> i32;
}
println!("Getting answer...");
let answer = get_answer();
println!("Answer is {}", answer);
Ardından kodunuzu WebAssembly olarak derleyin:
cargo build --target wasm32-unknown-unknown
Artık WebAssembly dosyasını, yığını depolamak/geri yüklemek için kodla donatmanız gerekir. C/C++ için Emscripten bunu bizim için yapar ancak burada kullanılmadığından süreç biraz daha manueldir.
Neyse ki Asyncify dönüştürme işlemi tamamen araç zincirine bağlı değildir. Hangi derleyici tarafından üretildiğine bakılmaksızın rastgele WebAssembly dosyalarını dönüştürebilir. Dönüşüm, Binaryen araç setindeki wasm-opt
optimize edici kapsamında ayrı olarak sağlanır ve şu şekilde çağrılabilir:
wasm-opt -O2 --asyncify \
--pass-arg=asyncify-imports@env.get_answer \
[...]
Dönüşümü etkinleştirmek için --asyncify
'ü iletin ve ardından program durumunun askıya alınması ve daha sonra devam ettirilmesi gereken, virgül ile ayrılmış bir eşzamansız işlev listesi sağlamak için --pass-arg=…
'ü kullanın.
Geriye kalan tek şey, WebAssembly kodunu askıya alıp devam ettiren destekleyici çalışma zamanı kodu sağlamaktır. Yine C / C++ durumunda bu Emscripten tarafından eklenir, ancak şimdi rastgele WebAssembly dosyalarını işleyecek özel JavaScript birleştirici koduna ihtiyacınız vardır. Bunun için bir kütüphane oluşturduk.
Bu aracı GitHub'da https://github.com/GoogleChromeLabs/asyncify adresinde veya npm'de asyncify-wasm
adıyla bulabilirsiniz.
Standart bir WebAssembly başlatma API'sini kendi ad alanının altında simüle eder. Tek fark, normal bir WebAssembly API'sinde yalnızca eşzamanlı işlevleri içe aktarma olarak sağlayabilmeniz, Asyncify sarmalayıcısında ise eşzamansız içe aktarma da sağlayabilmenizdir:
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();
Yukarıdaki örnekte belirtilen get_answer()
gibi eşzamansız bir işlevi WebAssembly tarafından çağırmayı denediğinizde, kitaplık döndürülen Promise
öğesini algılar, WebAssembly uygulamasının durumunu askıya alır ve kaydeder, vaat tamamlama işlemine abone olur ve ardından sorun çözüldükten sonra çağrı yığınını ve durumunu sorunsuzca geri yükler ve hiçbir şey olmamış gibi yürütmeye devam eder.
Modüldeki herhangi bir işlev eşzamansız çağrı yapabileceği için tüm dışa aktarma işlemleri de potansiyel olarak eşzamansız hale gelir ve bu nedenle sarmalanır. Yukarıdaki örnekte yürütmenin ne zaman tamamlandığını bilmek için instance.exports.main()
sonucunu await
işleminize ihtiyacınız olduğunu fark etmiş olabilirsiniz.
Bu sürecin işleyiş şekli nasıl?
Asyncify, ASYNCIFY_IMPORTS
işlevlerinden birine yapılan bir çağrıyı algıladığında, bir asenkron işlem başlatır, çağrı yığını ve geçici yerel değişkenler dahil olmak üzere uygulamanın tüm durumunu kaydeder ve daha sonra bu işlem tamamlandığında tüm belleği ve çağrı yığınını geri yükler ve program hiç durmamış gibi aynı yerden ve aynı durumda devam eder.
Bu, daha önce gösterdiğim JavaScript'teki async-await özelliğine oldukça benzer ancak JavaScript'teki özelliğin aksine, dilden özel bir söz dizimi veya çalışma zamanı desteği gerektirmez. Bunun yerine, derleme zamanında düz eşzamanlı işlevleri dönüştürerek çalışır.
Daha önce gösterilen zaman uyumsuz uyku örneğini derleyin:
puts("A");
async_sleep(1);
puts("B");
Asyncify bu kodu alıp yaklaşık olarak aşağıdaki gibi dönüştürür (sözde kod, gerçek dönüşüm bundan daha karmaşıktır):
if (mode == NORMAL_EXECUTION) {
puts("A");
async_sleep(1);
saveLocals();
mode = UNWINDING;
return;
}
if (mode == REWINDING) {
restoreLocals();
mode = NORMAL_EXECUTION;
}
puts("B");
Başlangıçta mode
NORMAL_EXECUTION
olarak ayarlanır. Buna uygun olarak, dönüştürülen kod ilk kez çalıştırıldığında yalnızca async_sleep()
öncesindeki bölüm değerlendirilir. Eşzamansız işlem planlanır planlanmaz Asyncify tüm yerel değişkenleri kaydeder ve her işlevden en tepeye dönerek yığını çözer. Böylece kontrolü tarayıcı etkinlik döngüsüne geri verir.
Ardından, async_sleep()
sorunu çözüldüğünde Asyncify destek kodu, mode
değerini REWINDING
olarak değiştirir ve işlevi tekrar çağırır. Bu sefer, "normal yürütme" dalı atlanır (çünkü iş son seferde zaten yapılmıştır ve "A"yı iki kez yazdırmaktan kaçınmak isterim) ve bunun yerine doğrudan "geri sarma" dalına gidilir. Koda ulaşıldığında depolanan tüm yerel verileri geri yükler, modu "normal" olarak değiştirir ve sanki kod hiç durdurulmamış gibi yürütmeye devam eder.
Dönüşüm maliyetleri
Maalesef Asyncify dönüşümü tamamen ücretsiz değildir. Tüm bu yerel değişkenleri depolamak ve geri yüklemek, farklı modlarda çağrı yığınında gezinmek vb. için oldukça fazla destek kodu eklemesi gerekir. Yalnızca komut satırında eşzamansız olarak işaretlenmiş işlevleri ve bunların olası çağrılarını değiştirmeye çalışır. Ancak kod boyutu ek yükünün, sıkıştırmadan önce yaklaşık% 50'ye varan oranda katkı sağlayabileceğini unutmayın.
Bu durum ideal değildir ancak alternatif olarak işlevin tamamen olmaması veya orijinal kodda önemli ölçüde yeniden yazma yapılması gerektiğinde çoğu durumda kabul edilebilir.
Daha da artmasını önlemek için nihai derlemelerde optimizasyonları her zaman etkinleştirdiğinizden emin olun. Dönüşümleri yalnızca belirtilen işlevlerle ve/veya yalnızca doğrudan işlev çağrılarıyla sınırlandırarak yükü azaltmak için Asyncify'ye özgü optimizasyon seçeneklerini de kontrol edebilirsiniz. Ayrıca çalışma zamanı performansında küçük bir maliyet de vardır ancak bu maliyet, asynkron çağrılarla sınırlıdır. Ancak gerçek çalışmanın maliyetine kıyasla genellikle önemsizdir.
Gerçek hayattan örnekler
Basit örnekleri incelediğinize göre şimdi daha karmaşık senaryolara geçeceğim.
Makalenin başında da belirtildiği gibi, web'deki depolama seçeneklerinden biri eşzamansız bir File System Access API'dir. Bir web uygulamasından gerçek bir ana makine dosya sistemine erişim sağlar.
Öte yandan, konsolda ve sunucu tarafında WebAssembly G/Ç için WASI adlı bir de facto standart vardır. Sistem dilleri için bir derleme hedefi olarak tasarlanmıştır ve her türlü dosya sistemi ve diğer işlemleri geleneksel senkronize biçimde gösterir.
Bunları birbirine eşleyebilseydiniz ne olurdu? Ardından, WASI hedefini destekleyen herhangi bir araç zinciriyle herhangi bir kaynak dilinde yazılmış herhangi bir uygulamayı derleyebilir ve gerçek kullanıcı dosyalarında çalışmasını sağlarken web'de bir korumalı alanda çalıştırabilirsiniz. Asyncify ile tam da bunu yapabilirsiniz.
Bu demoda, birkaç küçük yamayla Rust coreutils kasasını WASI'ye derledim, Asyncify dönüşümüyle iletildi ve JavaScript tarafında WASI'den File System Access API'ye eşzamansız bağlamalar uyguladım. Bu, Xterm.js terminal bileşeniyle birleştirildiğinde tarayıcı sekmesinde çalışan ve gerçek bir terminal gibi gerçek kullanıcı dosyaları üzerinde çalışan gerçekçi bir kabuk sağlar.
https://wasi.rreverser.com/ adresinden canlı olarak izleyebilirsiniz.
Asyncify kullanım alanları yalnızca zamanlayıcılar ve dosya sistemleriyle sınırlı değildir. Daha da ileri gidip web'de daha özel API'ler kullanabilirsiniz.
Örneğin, Asyncify'ın yardımıyla, muhtemelen USB cihazlarıyla çalışmak için kullanılan en popüler yerel kitaplık olan libusb'yu, web'de bu tür cihazlara eşzamansız erişim sağlayan WebUSB API ile eşleyebilirsiniz. Haritalandırıp derledikten sonra, seçili cihazlarda doğrudan bir web sayfasının korumalı alanında çalıştırılacak standart libusb testleri ve örnekleri elde ettim.
Ancak bu muhtemelen başka bir blog yazısı için hazırlanmış bir hikayedir.
Bu örnekler, Asyncify'nin boşluğu doldurmak ve her türlü uygulamayı web'e taşımak için ne kadar güçlü olabileceğini gösterir. Bu sayede, işlevselliği kaybetmeden platformlar arası erişim, korumalı alan ve daha iyi güvenlik elde edebilirsiniz.