Proposal ini dikembangkan bekerja sama dengan @fmccabe , @thibaudmichaud , @lukewagner , dan @kripken , bersama dengan umpan balik dari Stacks Subgroup (dengan pemungutan suara informal yang menyetujui memajukannya ke Fase 0 hari ini). Perhatikan bahwa, karena keterbatasan waktu, rencananya akan diadakan presentasi yang sangat cepat (yaitu 5 menit) dan pemungutan suara untuk memajukannya ke Tahap 1 pada tanggal 3 Agustus. Untuk memfasilitasi itu, kami sangat mendorong orang-orang untuk menyampaikan kekhawatiran di sini sebelumnya sehingga kami dapat menentukan apakah ada masalah utama yang perlu mendorong presentasi+suara kembali ke tanggal berikutnya dengan lebih banyak waktu.
Tujuan dari proposal ini adalah untuk menyediakan interop yang relatif efisien dan relatif ergonomis antara janji JavaScript dan WebAssembly tetapi bekerja di bawah batasan bahwa satu-satunya perubahan adalah pada JS API dan bukan pada core wasm.
Harapannya adalah proposal Stack-Switching pada akhirnya akan memperluas WebAssembly inti dengan fungsionalitas untuk mengimplementasikan operasi yang kami sediakan dalam proposal ini langsung di dalam WebAssembly, bersama dengan banyak operasi stack-switching berharga lainnya, tetapi kasus penggunaan khusus untuk stack switching ini telah urgensi yang cukup untuk mendapatkan jalur yang lebih cepat hanya melalui JS API.
Untuk informasi lebih lanjut, silakan lihat catatan dan slide untuk Pertemuan Subgrup Stack 28 Juni 2021 , yang merinci skenario penggunaan dan faktor yang kami pertimbangkan dan merangkum alasan bagaimana kami sampai pada desain berikut.
PEMBARUAN: Mengikuti umpan balik yang diterima Subgrup Tumpukan dari TC39, proposal ini hanya mengizinkan tumpukan WebAssembly untuk ditangguhkan—tidak membuat perubahan pada bahasa JavaScript dan, khususnya, tidak secara tidak langsung mengaktifkan dukungan untuk asycn
/ await
dalam JavaScript.
Ini tergantung (secara longgar) pada proposal js-types , yang memperkenalkan WebAssembly.Function
sebagai subkelas dari Function
.
Proposalnya adalah untuk menambahkan antarmuka, konstruktor, dan metode berikut ke JS API, dengan detail lebih lanjut tentang semantiknya di bawah ini.
interface Suspender {
constructor();
Function suspendOnReturnedPromise(Function func); // import wrapper
// overloaded: WebAssembly.Function suspendOnReturnedPromise(WebAssembly.Function func);
WebAssembly.Function returnPromiseOnSuspend(WebAssembly.Function func); // export wrapper
}
Berikut ini adalah contoh bagaimana kami mengharapkan seseorang menggunakan API ini.
Dalam skenario penggunaan kami, kami merasa berguna untuk mempertimbangkan modul WebAssembly untuk secara konseptual memiliki impor dan ekspor "sinkron" dan "asinkron".
JS API saat ini hanya mendukung impor dan ekspor "sinkron".
Metode antarmuka Suspender digunakan untuk membungkus impor dan ekspor yang relevan untuk membuat "asinkron", dengan objek Suspender itu sendiri secara eksplisit menghubungkan impor dan ekspor ini bersama-sama untuk memfasilitasi implementasi dan komposisi.
WebAssembly ( demo.wasm
):
(module
(import "js" "init_state" (func $init_state (result f64)))
(import "js" "compute_delta" (func $compute_delta (result f64)))
(global $state f64)
(func $init (global.set $state (call $init_state)))
(start $init)
(func $get_state (export "get_state") (result f64) (global.get $state))
(func $update_state (export "update_state") (result f64)
(global.set (f64.add (global.get $state) (call $compute_delta)))
(global.get $state)
)
)
Teks ( data.txt
):
19827.987
JavaScript:
var suspender = new Suspender();
var init_state = () => 2.71;
var compute_delta = () => fetch('data.txt').then(res => res.text()).then(txt => parseFloat(txt));
var importObj = {js: {
init_state: init_state,
compute_delta: suspender.suspendOnReturnedPromise(compute_delta)
}};
fetch('demo.wasm').then(response =>
response.arrayBuffer()
).then(buffer =>
WebAssembly.instantiate(buffer, importObj)
).then(({module, instance}) => {
var get_state = instance.exports.get_state;
var update_state = suspender.returnPromiseOnSuspend(instance.exports.update_state);
...
});
Dalam contoh ini, kami memiliki modul WebAssembly yang merupakan mesin status yang sangat sederhana—setiap kali Anda memperbarui status, itu hanya memanggil impor untuk menghitung delta untuk ditambahkan ke status.
Namun, di sisi JavaScript, fungsi yang ingin kita gunakan untuk menghitung delta ternyata perlu dijalankan secara asinkron; yaitu, ia mengembalikan Janji Angka daripada Angka itu sendiri.
Kami dapat menjembatani kesenjangan sinkronisasi ini dengan menggunakan JS API baru.
Dalam contoh, impor modul WebAssembly dibungkus menggunakan suspender.suspendOnReturnedPromise
, dan ekspor dibungkus menggunakan suspender.returnPromiseOnSuspend
, keduanya menggunakan suspender
.
suspender
terhubung ke keduanya bersama-sama.
Itu membuatnya sehingga, jika impor (terbuka) mengembalikan Janji, ekspor (terbungkus) mengembalikan Janji, dengan semua perhitungan di antaranya "ditangguhkan" hingga Janji impor diselesaikan.
Pembungkusan ekspor pada dasarnya adalah menambahkan penanda async
, dan pembungkusan impor pada dasarnya menambahkan penanda await
, tetapi tidak seperti JavaScript, kita tidak harus secara eksplisit memasukkan async
/ await
melalui semua fungsi WebAssembly perantara!
Sementara itu, panggilan yang dilakukan ke init_state
selama inisialisasi harus kembali tanpa penangguhan, dan panggilan ke ekspor get_state
juga selalu kembali tanpa penangguhan, sehingga proposal masih mendukung impor dan ekspor "sinkron" yang ada yang digunakan ekosistem WebAssembly saat ini.
Tentu saja, ada banyak detail yang diabaikan, seperti fakta bahwa jika ekspor sinkron memanggil impor asinkron maka program akan menjebak jika impor mencoba untuk menangguhkan.
Berikut ini memberikan spesifikasi yang lebih rinci serta beberapa strategi implementasi.
Suspender
berada di salah satu status berikut:
caller
] - kontrol ada di dalam Suspender
, dengan caller
menjadi fungsi yang dipanggil ke Suspender
dan mengharapkan externref
untuk dikembalikanMetode suspender.returnPromiseOnSuspend(func)
menegaskan bahwa func
adalah WebAssembly.Function
dengan tipe fungsi dari bentuk [ti*] -> [to]
dan kemudian mengembalikan WebAssembly.Function
dengan tipe fungsi [ti*] -> [externref]
yang melakukan hal berikut saat dipanggil dengan argumen args
:
suspender
tidak Aktifsuspender
menjadi Aktif [ caller
] (di mana caller
adalah pemanggil saat ini)result
menjadi hasil dari pemanggilan func(args)
(atau jebakan atau eksepsi yang dilempar)suspender
Aktif [ caller'
] untuk beberapa caller'
(harus dijamin, meskipun penelepon mungkin telah berubah)suspender
menjadi Tidak Aktifresult
ke caller'
Metode suspender.suspendOnReturnedPromise(func)
func
adalah WebAssembly.Function
, kemudian menegaskan bahwa tipe fungsinya dalam bentuk [t*] -> [externref]
dan mengembalikan WebAssembly.Function
dengan tipe fungsi [t*] -> [externref]
;func
adalah Function
dan mengembalikan Function
.Dalam kedua kasus tersebut, fungsi yang dikembalikan oleh suspender.suspendOnReturnedPromise(func)
melakukan hal berikut saat dipanggil dengan argumen args
:
result
menjadi hasil dari pemanggilan func(args)
(atau jebakan atau pengecualian apa pun yang dilemparkan)result
bukan Janji yang dikembalikan, maka kembalikan (atau rethrows) result
suspender
tidak Aktif [ caller
] untuk beberapa caller
frames
menjadi bingkai tumpukan karena caller
frames
suspender
menjadi Suspendedresult.then(onFulfilled, onRejected)
dengan fungsi onFulfilled
dan onRejected
yang melakukan hal berikut:suspender
Ditangguhkan (harus dijamin)suspender
menjadi Aktif [ caller'
], di mana caller'
adalah pemanggil onFulfilled
/ onRejected
onFulfilled
, mengonversi nilai yang diberikan menjadi externref
dan mengembalikannya ke frames
onRejected
, melempar nilai yang diberikan hingga frames
sebagai pengecualian menurut JS API dari proposal Penanganan PengecualianSuatu fungsi dapat ditangguhkan jika itu
suspendOnReturnedPromise
,returnPromiseOnSuspend
,Yang penting, fungsi yang ditulis dalam JavaScript tidak dapat ditangguhkan, sesuai dengan umpan balik dari anggota TC39 , dan fungsi host (kecuali untuk beberapa yang tercantum di atas) tidak dapat ditangguhkan, sesuai dengan umpan balik dari pengelola mesin.
Berikut ini adalah strategi implementasi proposal ini.
Ini mengasumsikan dukungan engine untuk stack-switching, yang tentu saja merupakan tantangan implementasi utama.
Ada dua jenis tumpukan: tumpukan host (dan JavaScript), dan tumpukan WebAssembly. Setiap tumpukan WebAssembly memiliki bidang suspender yang disebut suspender
. Setiap utas memiliki tumpukan host.
Setiap Suspender
memiliki dua bidang referensi tumpukan: satu disebut caller
dan satu lagi disebut suspended
.
caller
merujuk ke tumpukan (ditangguhkan) pemanggil, dan bidang suspended
adalah nolsuspended
merujuk ke tumpukan WebAssembly (ditangguhkan) yang saat ini terkait dengan penangguhan, dan bidang caller
adalah nol.suspender.returnPromiseOnSuspend(func)(args)
diimplementasikan oleh
suspender.caller
dan suspended.suspended
adalah null (menjebak sebaliknya)stack
menjadi tumpukan WebAssembly yang baru dialokasikan terkait dengan suspender
stack
dan menyimpan tumpukan sebelumnya di suspender.caller
result
menjadi hasil dari func(args)
(atau jebakan atau pengecualian apa pun yang dilemparkan)suspender.caller
dan menyetelnya ke nullstack
result
suspender.suspendOnReturnedPromise(func)(args)
diimplementasikan oleh
func(args)
, menangkap jebakan atau melempar pengecualianresult
bukan Janji yang dikembalikan, mengembalikan (atau melempar kembali) result
suspender.caller
bukan null (menjebak sebaliknya)stack
menjadi tumpukan saat inistack
bukan tumpukan WebAssembly yang terkait dengan suspender
:stack
adalah tumpukan WebAssembly (menjebak sebaliknya)stack
menjadi stack.suspender.caller
suspender.caller
, menyetelnya ke nol, dan menyimpan tumpukan sebelumnya di suspender.suspended
result.then(onFulfilled, onRejected)
dengan fungsi onFulfilled
dan onRejected
yang diimplementasikan olehsuspender.suspended
, setel ke null, dan simpan tumpukan sebelumnya di suspender.caller
onFulfilled
, mengonversi nilai yang diberikan menjadi externref
dan mengembalikannyaonRejected
, lemparkan kembali nilai yang diberikanImplementasi fungsi yang dihasilkan dengan membuat fungsi host untuk
Apakah mungkin untuk mengekspos API yang menerima fungsi/generator asinkron (sinkronisasi atau asinkron) lalu mengubahnya menjadi fungsi yang dapat ditangguhkan?
Bisakah Anda mengklarifikasi, mungkin dengan beberapa kode semu atau kasus penggunaan, apa yang Anda maksud? Saya ingin memastikan saya memberi Anda jawaban yang akurat.
Apakah maksud bahwa Suspender
akan menjadi bagian dari JS atau itu adalah API yang terpisah? Apakah ini khusus untuk wasm ( WebAssembly.Suspender
)? Menurut saya, proposal ini harus didiskusikan di TC39.
Ini secara khusus TIDAK dimaksudkan untuk mempengaruhi program JS. Lebih tepatnya, mencoba menangguhkan fungsi JS akan menghasilkan jebakan. Kami telah mengalami beberapa masalah untuk memastikan ini.
Namun, saya bisa membicarakannya dengan Shu-yu untuk mendapatkan pendapatnya.
Maaf, @chicoxyzzy , harus dapat menangkap bingkai JavaScript/host di tumpukan yang ditangguhkan. Namun, kami menerima umpan balik dari orang-orang di TC39 bahwa ada kekhawatiran bahwa hal ini akan mempengaruhi ekosistem JS secara drastis, dan kami menerima umpan balik dari pelaksana host bahwa ada kekhawatiran tidak semua frame host dapat mentolerir penangguhan. Jadi, Subgrup Tumpukan sejak itu memastikan desain hanya menangkap bingkai WebAssembly(terkait) di tumpukan yang ditangguhkan, dan proposal ini memenuhi properti itu. Saya memperbarui OP untuk memasukkan catatan penting ini.
Sangat menyenangkan melihat kemajuan di sini. Apakah ada contoh bagaimana ini akan digunakan dalam integrasi ESM untuk Wasm?
Berita buruknya adalah, karena ini semua ada di JS API, Anda tidak bisa begitu saja mengimpor modul wasm ESM dan mendapatkan dukungan pengalihan tumpukan ini untuk janji. Kabar baiknya adalah Anda masih dapat menggunakan modul ESM dengan API ini, hanya dengan beberapa modul JS ESM sebagai lem.
Secara khusus, Anda menyiapkan tiga modul ESM: foo-exports.js
, foo-wasm.wasm
, dan foo-imports.js
. Modul foo-imports.js
membuat suspender, menggunakannya untuk membungkus semua impor penghasil janji "asynchronous" yang dibutuhkan oleh foo-wasm.wasm
, dan mengekspor suspender dan impor tersebut. foo-wasm.wasm
lalu mengimpor semua impor "asinkron" dari foo-imports.js
dan semua impor "sinkron" langsung dari modulnya masing-masing (atau, tentu saja, Anda juga dapat mem-proksinya melalui foo-imports.js
, yang dapat mengekspornya tanpa membungkus). Terakhir, foo-exports.js
mengimpor suspender dari foo-imports.js
, mengimpor ekspor foo-wasm.wasm
, membungkus ekspor "asinkron" menggunakan suspender, dan kemudian mengekspor (membuka) "sinkron" ekspor dan ekspor "asinkron" yang dibungkus. Klien kemudian mengimpor dari foo-exports.js
dan tidak pernah secara langsung menyentuh (atau membutuhkan pengetahuan tentang) foo-wasm.wasm
atau foo-imports.js
.
Ini adalah rintangan yang tidak menguntungkan, tetapi ini adalah yang terbaik yang dapat kami capai mengingat batasan untuk tidak memodifikasi core wasm. Namun, kami bertujuan untuk memastikan bahwa desain ini kompatibel dengan proposal perluasan inti sedemikian rupa sehingga, ketika proposal itu dikirimkan, Anda dapat menukar ketiga modul ini dengan satu modul perluasan dan tidak ada yang dapat secara semantik membedakannya (penggantian nama file modulo).
Apakah itu dapat dimengerti, dan apakah menurut Anda itu akan memenuhi kebutuhan Anda (walaupun canggung)?
Saya memahami kebutuhan untuk membungkus, setidaknya sementara impor Wasm tipe WebAssembly.Module belum memungkinkan (dan mudah-mudahan mereka akan pada waktunya).
Lebih khusus lagi, saya bertanya-tanya apakah ada ruang untuk mendekorasi pola-pola ini sama sekali dalam integrasi ESM sehingga kedua sisi lem suspender bisa lebih diatur. Misalnya jika ada beberapa metadata yang menghubungkan fungsi yang diekspor dan diimpor dalam format biner, integrasi ESM dapat menginterogasinya dan mencocokkan fungsi suspender pembungkus impor / ekspor ganda secara internal sebagai bagian dari lapisan integrasi berdasarkan aturan tertentu yang dapat diprediksi.
Ah. Saat ini, tidak ada rencana seperti itu. Umpan balik yang saya terima adalah bahwa ada keinginan untuk tidak mengubah integrasi ESM juga. Singkatnya, harapannya adalah bahwa pada akhirnya semua ini akan mungkin terjadi di inti, jadi kami ingin proposal ini meninggalkan jejak sekecil mungkin.
Umpan balik yang saya terima adalah bahwa ada keinginan untuk tidak mengubah integrasi ESM juga
Bisakah Anda menjelaskan dari mana umpan balik ini berasal? Ada banyak ruang untuk memperluas integrasi ESM dengan semantik integrasi tingkat yang lebih tinggi, ruang yang saya rasa belum sepenuhnya dieksplorasi oleh karena itu mengapa saya mengangkatnya. Saya belum pernah mendengar tentang penolakan untuk memperbaiki area ini di masa lalu. Melihat ini sebagai area untuk gula dapat menjadi keuntungan bagi pengembang JS dalam mengizinkan impor / ekspor Promise langsung.
Perlu dicatat bahwa proposal ini memang menghalangi kemampuan satu modul JS dalam satu siklus untuk menjadi importir dan importir ke modul Wasm yang masih dapat bekerja saat ini untuk fungsi impor berkat peningkatan fungsi siklus JS dalam integrasi ESM , tetapi tidak akan mendukung pengangkatan siklus ini dengan pembungkus ekspresi Suspender di sekitar fungsi yang diimpor.
Saya mendapat kesan ini dari @lukewagner. Saya setuju ada ruang untuk memperluas integrasi ESM, tetapi pemahaman saya adalah bahwa ini memerlukan perubahan/ekstensi ke file wasm—yang kami coba hindari (sebagai bagian dari tujuan tapak kecil)—jadi kami tidak menginginkan perubahan/ ekstensi untuk menjadi bagian dari proposal ini. Tentu saja, jika perubahan/ekstensi tersebut ditambahkan ke proposal ESM, itu akan melengkapi proposal ini secara ideal sehingga orang tidak memerlukan modul pembungkus JS untuk mendapatkan fungsionalitas yang ditawarkan proposal ini.
Saya salah membaca komentar @Jack-Works, telah menyesuaikan komentar saya di atas.
Terima kasih @RossTate atas klarifikasinya, ya, saya menyarankan untuk menjelajahi kemungkinan mencocokkan konteks penangguhan impor dan ekspor ini melalui metadata dalam biner itu sendiri untuk menginformasikan integrasi Host, tetapi tidak mengharapkan itu di MVP dengan cara apa pun. Saya juga hanya mengambil kesempatan untuk menunjukkan integrasi ESM adalah ruang yang mungkin mendapat manfaat dari gula secara lebih umum, secara terpisah ke JS API dasar.
Untuk lebih jelasnya, tantangan yang saya tunjukkan adalah bahwa opsi apa pun yang kami tambahkan ke WebAssembly.instantiate()
(atau versi baru WebAssembly.instantiate()
dengan parameter baru) juga harus muncul ketika wasm dimuat melalui ESM -integrasi, bukan berarti integrasi ESM tidak dapat diubah.
Ah, keren, jadi kami memiliki lebih banyak fleksibilitas mengenai ESM daripada yang saya sadari, jika diperlukan. Terima kasih telah mengoreksi kesalahpahaman saya.
Sepertinya kita sedang berbicara tentang semacam bagian khusus untuk menentukan bagaimana fungsi Wasm yang diekspor tertentu harus muncul ke JS sebagai API berbasis Janji, dan mungkin sebaliknya bagaimana impor dari Wasm dapat dikonversi dari API berbasis JS Janji ke beberapa jenis dari peralihan tumpukan. Apakah saya memahami dengan benar?
Aku suka ide ini. Saya menduga kita akan menemukan diri kita menginginkan bagian kustom analog untuk integrasi Wasm GC/JS-ESM (atau bagian dari yang sama). Saya tidak yakin sejauh mana bagian kustom ini mungkin lintas bahasa, tetapi dalam kedua kasus, itu mungkin sedikit kurang universal daripada tipe antarmuka, dan juga cenderung digunakan dalam komponen, tidak hanya di antara mereka.
Adakah yang ingin menulis semacam Intisari atau README yang menjelaskan desain dasar untuk bagian khusus ini?
Sepertinya itu adalah opsi yang memungkinkan. Seperti yang Anda sebutkan, opsi serupa telah dibahas dalam proposal GC, seperti di WebAssembly/gc#203. Integrasi JS dijadwalkan untuk didiskusikan dalam subkelompok GC besok, jadi mungkin baik untuk mengingat kemungkinan koneksi ke proposal ini selama diskusi itu (atau mungkin terbukti tidak terkait, tergantung pada bagaimana diskusi berjalan).