Design: Proposal: Async/Menunggu JS API

Dibuat pada 26 Jul 2021  ·  16Komentar  ·  Sumber: WebAssembly/design

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 .

Antarmuka

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
}

Contoh

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.

Spesifikasi

Suspender berada di salah satu status berikut:

  • Tidak aktif - tidak digunakan saat ini
  • Aktif [ caller ] - kontrol ada di dalam Suspender , dengan caller menjadi fungsi yang dipanggil ke Suspender dan mengharapkan externref untuk dikembalikan
  • Ditangguhkan - saat ini menunggu beberapa janji untuk diselesaikan

Metode 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 :

  1. Jebakan jika status suspender tidak Aktif
  2. Mengubah status suspender menjadi Aktif [ caller ] (di mana caller adalah pemanggil saat ini)
  3. Biarkan result menjadi hasil dari pemanggilan func(args) (atau jebakan atau eksepsi yang dilempar)
  4. Menyatakan bahwa status suspender Aktif [ caller' ] untuk beberapa caller' (harus dijamin, meskipun penelepon mungkin telah berubah)
  5. Mengubah status suspender menjadi Tidak Aktif
  6. Mengembalikan (atau rethrows) result ke caller'

Metode suspender.suspendOnReturnedPromise(func)

  • jika func adalah WebAssembly.Function , kemudian menegaskan bahwa tipe fungsinya dalam bentuk [t*] -> [externref] dan mengembalikan WebAssembly.Function dengan tipe fungsi [t*] -> [externref] ;
  • jika tidak, menegaskan bahwa 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 :

  1. Biarkan result menjadi hasil dari pemanggilan func(args) (atau jebakan atau pengecualian apa pun yang dilemparkan)
  2. Jika result bukan Janji yang dikembalikan, maka kembalikan (atau rethrows) result
  3. Jebakan jika status suspender tidak Aktif [ caller ] untuk beberapa caller
  4. Biarkan frames menjadi bingkai tumpukan karena caller
  5. Jebakan jika ada bingkai fungsi yang tidak dapat ditangguhkan di frames
  6. Mengubah status suspender menjadi Suspended
  7. Mengembalikan hasil result.then(onFulfilled, onRejected) dengan fungsi onFulfilled dan onRejected yang melakukan hal berikut:

    1. Menegaskan bahwa status suspender Ditangguhkan (harus dijamin)

    2. Mengubah status suspender menjadi Aktif [ caller' ], di mana caller' adalah pemanggil onFulfilled / onRejected



      • Dalam kasus onFulfilled , mengonversi nilai yang diberikan menjadi externref dan mengembalikannya ke frames


      • Dalam kasus onRejected , melempar nilai yang diberikan hingga frames sebagai pengecualian menurut JS API dari proposal Penanganan Pengecualian



Suatu fungsi dapat ditangguhkan jika itu

  • didefinisikan oleh modul WebAssembly,
  • dikembalikan oleh suspendOnReturnedPromise ,
  • dikembalikan oleh returnPromiseOnSuspend ,
  • atau dihasilkan dengan membuat fungsi host untuk

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.

Penerapan

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 .

  • Dalam status Tidak Aktif , kedua bidang adalah nol.
  • Dalam status Aktif , bidang caller merujuk ke tumpukan (ditangguhkan) pemanggil, dan bidang suspended adalah nol
  • Dalam status Ditangguhkan , bidang suspended merujuk ke tumpukan WebAssembly (ditangguhkan) yang saat ini terkait dengan penangguhan, dan bidang caller adalah nol.

suspender.returnPromiseOnSuspend(func)(args) diimplementasikan oleh

  1. Memeriksa apakah suspender.caller dan suspended.suspended adalah null (menjebak sebaliknya)
  2. Membiarkan stack menjadi tumpukan WebAssembly yang baru dialokasikan terkait dengan suspender
  3. Beralih ke stack dan menyimpan tumpukan sebelumnya di suspender.caller
  4. Membiarkan result menjadi hasil dari func(args) (atau jebakan atau pengecualian apa pun yang dilemparkan)
  5. Beralih ke suspender.caller dan menyetelnya ke null
  6. Membebaskan stack
  7. Mengembalikan (atau melempar kembali) result

suspender.suspendOnReturnedPromise(func)(args) diimplementasikan oleh

  1. Memanggil func(args) , menangkap jebakan atau melempar pengecualian
  2. Jika result bukan Janji yang dikembalikan, mengembalikan (atau melempar kembali) result
  3. Memeriksa bahwa suspender.caller bukan null (menjebak sebaliknya)
  4. Biarkan stack menjadi tumpukan saat ini
  5. Sementara stack bukan tumpukan WebAssembly yang terkait dengan suspender :

    • Memeriksa bahwa stack adalah tumpukan WebAssembly (menjebak sebaliknya)

    • Memperbarui stack menjadi stack.suspender.caller

  6. Beralih ke suspender.caller , menyetelnya ke nol, dan menyimpan tumpukan sebelumnya di suspender.suspended
  7. Mengembalikan hasil result.then(onFulfilled, onRejected) dengan fungsi onFulfilled dan onRejected yang diimplementasikan oleh

    1. Beralih ke suspender.suspended , setel ke null, dan simpan tumpukan sebelumnya di suspender.caller



      • Dalam kasus onFulfilled , mengonversi nilai yang diberikan menjadi externref dan mengembalikannya


      • Dalam kasus onRejected , lemparkan kembali nilai yang diberikan



Implementasi fungsi yang dihasilkan dengan membuat fungsi host untuk

Semua 16 komentar

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).

Apakah halaman ini membantu?
0 / 5 - 0 peringkat

Masalah terkait

badumt55 picture badumt55  ·  8Komentar

Artur-A picture Artur-A  ·  3Komentar

bobOnGitHub picture bobOnGitHub  ·  6Komentar

beriberikix picture beriberikix  ·  7Komentar

chicoxyzzy picture chicoxyzzy  ·  5Komentar