تم تطوير هذا الاقتراح بالتعاون مع fmccabe و thibaudmichaud و lukewagner و kripken ، جنبًا إلى جنب مع التعليقات من Stacks Subgroup (مع تصويت غير رسمي بالموافقة على التقدم إلى المرحلة 0 اليوم). لاحظ أنه نظرًا لضيق الوقت ، فإن الخطة هي تقديم عرض تقديمي سريع جدًا (أي 5 دقائق) والتصويت للتقدم إلى المرحلة 1 في 3 أغسطس. لتسهيل ذلك ، نشجع الأشخاص بشدة على إثارة مخاوفهم هنا في وقت مبكر حتى نتمكن من تحديد ما إذا كانت هناك أي مخاوف كبيرة تستحق دفع العرض التقديمي + التصويت إلى تاريخ لاحق مع مزيد من الوقت.
الغرض من هذا الاقتراح هو توفير تفاعل فعال نسبيًا ومتوافقًا نسبيًا بين وعود JavaScript و WebAssembly ولكن مع العمل في ظل القيود التي تنص على أن التغييرات الوحيدة هي على JS API وليس إلى Core wasm.
من المتوقع أن يؤدي اقتراح Stack-Switching في النهاية إلى توسيع WebAssembly الأساسي بوظيفة تنفيذ العمليات التي نقدمها في هذا الاقتراح مباشرةً داخل WebAssembly ، جنبًا إلى جنب مع العديد من عمليات تبديل المكدس القيمة الأخرى ، ولكن حالة الاستخدام الخاصة هذه للتبديل المكدس كانت الاستعجال الكافي لاستحقاق مسار أسرع عبر JS API فقط.
لمزيد من المعلومات ، يرجى الرجوع إلى الملاحظات والشرائح الخاصة باجتماع المجموعة الفرعية Stack في 28 يونيو 2021 ، والتي توضح بالتفصيل سيناريوهات الاستخدام والعوامل التي أخذناها في الاعتبار وتلخص الأساس المنطقي لكيفية وصولنا إلى التصميم التالي.
تحديث: بعد التعليقات التي تلقتها مجموعة Stacks الفرعية من TC39 ، فإن هذا الاقتراح يسمح فقط بتعليق حزم WebAssembly - لا يُجري أي تغييرات على لغة JavaScript ، ولا يُمكّن بشكل غير مباشر الدعم المنفصل asycn
/ await
في JavaScript.
يعتمد هذا (بشكل فضفاض) على اقتراح أنواع js ، والذي يقدم WebAssembly.Function
كفئة فرعية من Function
.
الاقتراح هو إضافة الواجهة والمنشئ والطرق التالية إلى JS API ، مع مزيد من التفاصيل حول دلالاتها أدناه.
interface Suspender {
constructor();
Function suspendOnReturnedPromise(Function func); // import wrapper
// overloaded: WebAssembly.Function suspendOnReturnedPromise(WebAssembly.Function func);
WebAssembly.Function returnPromiseOnSuspend(WebAssembly.Function func); // export wrapper
}
فيما يلي مثال على كيف نتوقع أن يستخدم المرء واجهة برمجة التطبيقات هذه.
في سيناريوهات الاستخدام الخاصة بنا ، وجدنا أنه من المفيد النظر في وحدات WebAssembly في أنها تشتمل على عمليات استيراد وتصدير "متزامنة" و "غير متزامنة".
تدعم واجهة برمجة تطبيقات JS الحالية عمليات الاستيراد والتصدير "المتزامنة" فقط.
تُستخدم أساليب واجهة Suspender لتغليف الواردات والصادرات ذات الصلة من أجل جعل "غير متزامن" ، حيث يربط كائن Suspender نفسه صراحةً هذه الواردات والصادرات معًا لتسهيل كل من التنفيذ والتركيب.
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)
)
)
نص ( data.txt
):
19827.987
جافا سكريبت:
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);
...
});
في هذا المثال ، لدينا وحدة WebAssembly التي تعد آلة حالة مبسطة للغاية - في كل مرة تقوم فيها بتحديث الحالة ، تقوم ببساطة باستدعاء استيراد لحساب دلتا لإضافتها إلى الحالة.
من ناحية جافا سكريبت ، تبين أن الوظيفة التي نريد استخدامها لحساب دلتا يجب أن تعمل بشكل غير متزامن ؛ أي أنه يُرجع وعدًا برقم بدلاً من رقم نفسه.
يمكننا سد فجوة التزامن هذه باستخدام JS API الجديدة.
في المثال ، يتم تغليف استيراد وحدة WebAssembly باستخدام suspender.suspendOnReturnedPromise
، ويتم تغليف التصدير باستخدام suspender.returnPromiseOnSuspend
، وكلاهما يستخدم نفس suspender
.
هذا suspender
يتصل بالاثنين معًا.
يجعل الأمر كذلك ، في حالة إرجاع الاستيراد (غير المغلف) لوعدًا ، يقوم التصدير (المغلف) بإرجاع الوعد ، مع "تعليق" جميع الحسابات بينهما حتى يتم حل وعد الاستيراد.
يؤدي التفاف التصدير بشكل أساسي إلى إضافة علامة async
، كما أن التفاف الاستيراد يضيف بشكل أساسي علامة await
، ولكن على عكس JavaScript ، لا يتعين علينا صراحة وضع مؤشر ترابط async
/ await
على طول الطريق من خلال جميع وظائف WebAssembly الوسيطة!
وفي الوقت نفسه ، فإن الاستدعاء الذي تم إجراؤه إلى init_state
أثناء التهيئة يعود بالضرورة دون تعليق ، كما أن المكالمات إلى التصدير get_state
تعود دائمًا دون تعليق ، لذلك لا يزال الاقتراح يدعم عمليات الاستيراد والتصدير "المتزامنة" الحالية يستخدم النظام البيئي WebAssembly اليوم.
بالطبع ، هناك العديد من التفاصيل التي يتم تجاهلها ، مثل حقيقة أنه إذا كان التصدير المتزامن يستدعي استيرادًا غير متزامن ، فسيقوم البرنامج بالاعتراض إذا حاول الاستيراد التعليق.
يوفر ما يلي مواصفات أكثر تفصيلاً بالإضافة إلى بعض إستراتيجيات التنفيذ.
A Suspender
في إحدى الحالات التالية:
caller
] - عنصر التحكم داخل Suspender
، حيث caller
هي الوظيفة التي استدعت إلى Suspender
وتتوقع externref
يعاد تؤكد الطريقة suspender.returnPromiseOnSuspend(func)
أن func
هو WebAssembly.Function
مع نوع دالة على شكل [ti*] -> [to]
ثم تُرجع WebAssembly.Function
بنوع الوظيفة [ti*] -> [externref]
يقوم بما يلي عند استدعائه بالوسيطات args
:
suspender
غير نشطةsuspender
إلى نشط [ caller
] (حيث caller
هو المتصل الحالي)result
نتيجة استدعاء func(args)
(أو أي فخ أو استثناء تم إلقاؤه)suspender
نشطة [ caller'
] لبعض caller'
(يجب ضمان ذلك ، على الرغم من أن المتصل قد تغير)suspender
إلى غير نشطresult
إلى caller'
الطريقة suspender.suspendOnReturnedPromise(func)
func
هو WebAssembly.Function
، فإنه يؤكد أن نوع وظيفته على الشكل [t*] -> [externref]
وإرجاع WebAssembly.Function
مع نوع الوظيفة [t*] -> [externref]
؛func
هو Function
وإرجاع Function
.في كلتا الحالتين ، تقوم الدالة التي تم إرجاعها بواسطة suspender.suspendOnReturnedPromise(func)
بما يلي عند استدعائها باستخدام الوسيطات args
:
result
نتيجة استدعاء func(args)
(أو أي اعتراض أو استثناء تم إلقاؤه)result
وعدًا مرتجعًا ، فحينئذٍ يُرجع (أو يعيد حفظه) result
suspender
غير نشطة [ caller
] لبعض caller
frames
إطارات المكدس منذ caller
frames
suspender
إلى معلقresult.then(onFulfilled, onRejected)
مع الدالات onFulfilled
و onRejected
التي تقوم بما يلي:suspender
معلقة (يجب ضمانها)suspender
إلى نشط [ caller'
] ، حيث caller'
هو المتصل بـ onFulfilled
/ onRejected
onFulfilled
، يتم تحويل القيمة المحددة إلى externref
وإرجاعها إلى frames
onRejected
، يتم طرح القيمة المحددة حتى frames
كاستثناء وفقًا لواجهة برمجة تطبيقات JS لاقتراح معالجة الاستثناءاتتكون الوظيفة قابلة للتعليق إذا كانت كذلك
suspendOnReturnedPromise
،returnPromiseOnSuspend
،الأهم من ذلك ، أن الوظائف المكتوبة في JavaScript غير قابلة للتعليق ، وتتوافق مع التعليقات الواردة من أعضاء TC39 ، ووظائف المضيف (باستثناء القليل منها المذكورة أعلاه) غير قابلة للتعليق ، بما يتوافق مع التعليقات الواردة من المشرفين على صيانة المحرك.
فيما يلي إستراتيجية تنفيذ لهذا الاقتراح.
إنه يفترض دعم المحرك للتبديل المكدس ، وهو بالطبع المكان الذي تكمن فيه تحديات التنفيذ الرئيسية.
هناك نوعان من الحزم: مكدس مضيف (وجافا سكريبت) ومكدس WebAssembly. يحتوي كل حزمة WebAssembly على حقل معلق يسمى suspender
. كل موضوع له مكدس مضيف.
كل Suspender
يحتوي على حقلين مرجعيين للمكدس: أحدهما يسمى caller
والآخر يسمى suspended
.
caller
إلى المكدس (المعلق) للمتصل ، والحقل suspended
خالٍsuspended
المراجع حقل (مع وقف التنفيذ) WebAssembly كومة ترتبط حاليا مع حمالة، و caller
الحقل فارغة.suspender.returnPromiseOnSuspend(func)(args)
تم تنفيذه بواسطة
suspender.caller
و suspended.suspended
فارغين (وإلا فإن الملاءمة)stack
أن يكون مكدس WebAssembly المخصص حديثًا والمرتبط بـ suspender
stack
وتخزين المكدس السابق في suspender.caller
result
هو نتيجة func(args)
(أو أي فخ أو استثناء تم إلقاؤه)suspender.caller
وتعيينه على قيمة خاليةstack
result
suspender.suspendOnReturnedPromise(func)(args)
بواسطة
func(args)
، اصطياد أي فخ أو استثناءresult
وعدًا مرتجعًا ، فالإرجاع (أو إعادة الإسقاط) result
suspender.caller
ليس فارغًا (الملاءمة وإلا)stack
هو المكدس الحاليstack
ليس حزمة WebAssembly المرتبطة بـ suspender
:stack
عبارة عن مكدس WebAssembly (الملاءمة بخلاف ذلك)stack
ليصبح stack.suspender.caller
suspender.caller
، وتعيينه على null ، وتخزين المكدس السابق في suspender.suspended
result.then(onFulfilled, onRejected)
بالوظائف onFulfilled
و onRejected
التي تم تنفيذها بواسطةsuspender.suspended
، وتعيينه على قيمة خالية ، وتخزين المكدس السابق في suspender.caller
onFulfilled
، يتم تحويل القيمة المعطاة إلى externref
وإعادتهاonRejected
، إعادة طرح القيمة المعطاةيتم تغيير تنفيذ الوظيفة التي تم إنشاؤها عن طريق إنشاء وظيفة مضيفة لوظيفة قابلة للتعليق للتبديل أولاً إلى مكدس المضيف لمؤشر الترابط الحالي (إذا لم يكن موجودًا بالفعل) وللرجوع أخيرًا إلى المكدس السابق.
هل من الممكن كشف واجهة برمجة تطبيقات تتلقى وظيفة / مولد غير متزامن (مزامنة أو غير متزامن) ثم تحويلها إلى وظيفة قابلة للتعليق؟
هل يمكنك أن توضح ، ربما ببعض الشفرة الزائفة أو حالة الاستخدام ، ماذا تقصد؟ أريد أن أتأكد من أنني أعطيك إجابة دقيقة.
هل النية أن يكون Suspender
جزءًا من JS أم أنه واجهة برمجة تطبيقات منفصلة؟ هل هو حصري لـ wasm ( WebAssembly.Suspender
)؟ يبدو لي أنه ينبغي مناقشة هذا الاقتراح في TC39.
لا يُقصد به على وجه التحديد التأثير على برامج JS. بتعبير أدق ، ستؤدي محاولة تعليق دالة JS إلى حدوث اعتراض. لقد واجهنا بعض المشاكل لضمان ذلك.
ومع ذلك ، يمكنني رفعه مع Shu-yu للحصول على رأيه.
عذرًا ، chicoxyzzy ، أرى أنني نسيت تضمين بعض السياق / التحديثات من مجموعة Stacks الفرعية. تمت كتابة مقترحات تبديل المكدس الأقدم مع توقع أنه يجب أن تكون قادرًا على التقاط إطارات JavaScript / مضيفة في مكدسات معلقة. ومع ذلك ، تلقينا تعليقات من الأشخاص في TC39 مفادها أن هناك مخاوف من أن هذا سيؤثر بشكل كبير على نظام JS البيئي ، وتلقينا تعليقات من منفذي مضيفين مفادها أن هناك مخاوف من عدم قدرة جميع الأطر المضيفة على تحمل التعليق. لذا ، فإن مجموعة Stacks الفرعية منذ ذلك الحين تضمن أن التصميمات لا تلتقط سوى إطارات WebAssembly (المرتبطة) في مكدسات معلقة ، وهذا الاقتراح يلبي هذه الخاصية. لقد قمت بتحديث OP لتضمين هذه الملاحظة الهامة.
إنه لأمر رائع أن نرى التقدم هنا. هل هناك أي أمثلة على كيفية استخدام ذلك في تكامل ESM في Wasm؟
الأخبار السيئة هي أنه نظرًا لأن هذا كله موجود في JS API ، فلا يمكنك ببساطة استيراد وحدة ESM وحصل على دعم تبديل المكدس للوعود. والخبر السار هو أنه لا يزال بإمكانك استخدام وحدات ESM مع واجهة برمجة التطبيقات هذه ، فقط مع بعض وحدات JS ESM كلصق.
على وجه الخصوص ، قمت بإعداد ثلاث وحدات ESM: foo-exports.js
، foo-wasm.wasm
، و foo-imports.js
. تقوم الوحدة النمطية foo-imports.js
بإنشاء الحمّالة ، وتستخدمها لتغليف جميع الواردات "غير المتزامنة" التي تنتج الوعد والتي تتطلبها foo-wasm.wasm
، وتقوم بتصدير الحمّالة وتلك الواردات. foo-wasm.wasm
بعد ذلك باستيراد جميع عمليات الاستيراد "غير المتزامنة" من foo-imports.js
وجميع عمليات الاستيراد "المتزامنة" مباشرةً من الوحدات النمطية الخاصة بها (أو ، بالطبع ، يمكنك أيضًا توكيلها من خلال foo-imports.js
، والذي يمكنه تصديرها بدون تغليف). أخيرًا ، يستورد foo-exports.js
المعلق من foo-imports.js
، ويستورد صادرات foo-wasm.wasm
، ويلتف الصادرات "غير المتزامنة" باستخدام الحمالة ، ثم يصدر (غير مغلف) "متزامن" الصادرات والصادرات المغلفة "غير المتزامنة". يقوم العملاء بعد ذلك بالاستيراد من foo-exports.js
ولا يلمسوا (أو يحتاجون إلى معرفة) مباشرةً (أو يحتاجون إلى معرفة) foo-wasm.wasm
أو foo-imports.js
.
إنها عقبة مؤسفة ، لكنها كانت أفضل ما يمكن أن نحققه بالنظر إلى قيود عدم تعديل النواة الأساسية. نحن نهدف إلى التأكد ، على الرغم من ذلك ، من أن هذا التصميم متوافق إلى الأمام مع اقتراح توسيع النطاق الأساسي بطريقة ، عندما يتم شحن هذا الاقتراح ، يمكنك تبديل هذه الوحدات الثلاث لوحدة واحدة ممتدة ولا يمكن لأي شخص أن يكون لها دلالة أخبر الفرق (إعادة تسمية ملف modulo).
هل كان ذلك مفهومًا ، وهل تعتقد أنه سيخدم احتياجاتك (وإن كان محرجًا)؟
أتفهم الحاجة إلى التغليف ، على الأقل أثناء استيراد WebAssembly.Module من نوع Wasm غير ممكن بعد (وآمل أن يكون ذلك في الوقت المناسب).
بشكل أكثر تحديدًا ، كنت أتساءل عما إذا كان هناك مجال لتزيين هذه الأنماط على الإطلاق في تكامل ESM بحيث يمكن إدارة كلا جانبي غراء الحمالة بشكل أكبر. على سبيل المثال ، إذا كانت هناك بعض البيانات التعريفية التي ربطت بين الوظائف المصدرة والمستوردة بالتنسيق الثنائي ، فيمكن لتكامل ESM أن يستفسر عن ذلك ويتطابق مع وظائف تعليق التغليف المزدوج للاستيراد / التصدير داخليًا كجزء من طبقة التكامل استنادًا إلى قواعد معينة يمكن التنبؤ بها.
آه. في الوقت الحاضر ، لا توجد خطة من هذا القبيل في مكانها الصحيح. كانت التعليقات التي تلقيتها هي أن هناك رغبة في عدم تغيير تكامل ESM أيضًا. باختصار ، الأمل هو أنه في النهاية سيكون كل هذا ممكنًا في الصميم الأساسي ، ولذا نريد أن يترك هذا الاقتراح بصمة صغيرة قدر الإمكان.
كانت التعليقات التي تلقيتها هي أن هناك رغبة في عدم تغيير تكامل ESM أيضًا
هل يمكنك التعمق في معرفة مصدر هذه الملاحظات؟ هناك مجال كبير لتوسيع تكامل ESM مع دلالات تكامل ذات مستوى أعلى ، وهي مساحة لا أشعر بها قد تم استكشافها بشكل كامل ، ومن ثم فإنني أطرحها. لم أسمع عن مقاومة لتحسين هذا المجال في الماضي. يمكن أن تكون رؤية هذا كمنطقة للحلاوة مفيدة لمطوري JS في السماح باستيراد / تصدير Promise المباشر.
تجدر الإشارة إلى أن هذا الاقتراح يعيق القدرة على أن تكون وحدة JS واحدة في دورة ما هي المستورد والمستورد إلى وحدة Wasm والتي لا يزال بإمكانها العمل في الوقت الحالي لاستيراد الوظائف بفضل رفع وظيفة دورة JS في تكامل ESM ، ولكن لن تدعم هذه الدورة الرفع باستخدام غلاف تعبير Suspender حول الوظيفة المستوردة.
حصلت على هذا الانطباع منlukewagner. أوافق على وجود مجال لتوسيع تكامل ESM ، لكن ما أفهمه هو أن هذا يتطلب تغييرات / امتدادات لملف wasm - الذي كنا نحاول تجنبه (كجزء من هدف المساحة الصغيرة) - لذلك لم نرغب في مثل هذه التغييرات / لتكون جزءًا من هذا الاقتراح. بالطبع ، إذا تمت إضافة هذه التغييرات / الامتدادات إلى اقتراح ESM ، فستكمل هذه التغييرات / الامتدادات بشكل مثالي هذا الاقتراح بحيث لا يحتاج المرء إلى وحدات غلاف JS للحصول على الوظيفة التي يوفرها هذا الاقتراح.
لقد أخطأت في قراءة تعليق @ Jack-Works ، وقمت بتعديل تعليقي أعلاه.
شكرًا RossTate على التوضيحات ، نعم أقترح استكشاف إمكانية مطابقة سياقات تعليق الاستيراد والتصدير عبر البيانات الوصفية في الملف الثنائي نفسه لإبلاغ تكامل المضيف ، ولكن لا أتوقع ذلك في MVP بأي وسيلة. أنا أيضًا أغتنم الفرصة للإشارة إلى أن تكامل ESM هو مساحة قد تستفيد من السكر بشكل عام ، بشكل منفصل عن JS API الأساسي.
للتوضيح ، كان التحدي الذي أشرت إليه هو أن أي خيارات أضفناها إلى WebAssembly.instantiate()
(أو الإصدارات الجديدة من WebAssembly.instantiate()
مع معلمات جديدة) يجب أن تظهر بطريقة ما عند تحميل wasm عبر ESM - التكامل ، وليس أن التكامل ESM كان ثابتًا.
آه ، رائع ، لذلك لدينا مرونة أكبر فيما يتعلق بالإدارة السليمة بيئياً أكثر مما أدركت ، إذا دعت الحاجة. شكرا لتصحيح سوء فهمي.
يبدو أننا نتحدث عن نوع من الأقسام المخصصة لتحديد كيفية ظهور بعض وظائف Wasm المُصدرة في JS كواجهات برمجة تطبيقات مستندة إلى Promise ، وربما على العكس من ذلك ، كيف يمكن تحويل الواردات من Wasm من واجهات برمجة التطبيقات المستندة إلى JS Promise إلى نوع ما من كومة التبديل. هل أفهم بشكل صحيح؟
اعجبتني هذه الفكرة. أظن أننا سنجد أنفسنا نريد قسمًا مخصصًا مشابهًا لتكامل Wasm GC / JS-ESM (أو جزء من نفس القسم). لست متأكدًا إلى أي مدى قد يكون هذا القسم المخصص متعدد اللغات ، ولكن في كلتا الحالتين ، من المحتمل أن يكون أقل عمومية قليلاً من أنواع الواجهة ، ويميل أيضًا إلى استخدامه داخل أحد المكونات ، وليس فقط بينهما.
هل يريد أي شخص كتابة نوع من الجوهر أو التمهيدي لوصف التصميم الأساسي لهذا القسم المخصص؟
يبدو أن هذا خيار ممكن. كما ذكرت ، تمت مناقشة خيارات مماثلة في اقتراح GC ، كما هو الحال في WebAssembly / gc # 203. من المقرر مبدئيًا مناقشة تكامل JS في المجموعة الفرعية GC في المجموعة الفرعية غدًا ، لذلك قد يكون من الجيد الاحتفاظ بالاتصال المحتمل بهذا الاقتراح في الاعتبار أثناء تلك المناقشة (أو قد يتبين أنه غير ذي صلة ، اعتمادًا على كيفية سير المناقشة).