WebAssembly และ C++

ตามที่สัญญาไว้เป็นการติดตาม บันทึกย่อของผู้เริ่มต้นของฉันเกี่ยวกับ WebAssembly ต่อไป นี้เป็นบันทึกย่อบางส่วนเกี่ยวกับ WebAssembly โดยเฉพาะเกี่ยวกับ C/C++

เอ็มสคริปเทน เสียงดังกราวมีการสนับสนุนในตัวสำหรับการสร้างรหัส Wasm แต่ยังมีอีกมากในการสร้างรหัสที่มากกว่าแค่การแปล C++ เป็นรหัสเครื่องที่เหมาะสม Emscripten เป็น C++ ไปยังเบราว์เซอร์ Wasm toolchain ที่จัดการสิ่งนี้ ให้คุณมีคอมไพเลอร์ emcc ที่เปลี่ยนจากแหล่ง C ไปเป็นเอาต์พุต . .wasm สำหรับตัวอย่างบางส่วน emscripten ได้จัดเตรียมตัวเชื่อมโยงและการใช้งาน malloc เช่นเดียวกับการสนับสนุนไฟล์ .js ที่จำเป็นสำหรับการโหลด wasm และมันยังไปไกลถึงการสร้างไฟล์ HTML อีกด้วย

สิ่งที่ยิ่งใหญ่อื่น ๆ ที่ Emscripten จัดการคือการทำให้รหัส C ++ ที่มีอยู่ทำงานโดยทำสิ่งต่าง ๆ เช่น shimming การโทร C++ เช่น puts() ในการเรียก JS console.log() อีกตัวอย่างหนึ่ง เมื่อคุณเขียนโค้ด C++ GL แสดงว่า C++ GL API บนเบราว์เซอร์ WebGL เรียก และในทำนองเดียวกันสำหรับ C++ API อื่นๆ จำนวนมาก ดู ส่วนเอกสารของพวกเขาใน Porting แต่สำหรับวิธีที่ Figma ใช้ C++ เราไม่ค่อยได้ใช้สิ่งนี้มากนัก เนื่องจากโดยทั่วไปแล้วเราจะเขียนโค้ดตั้งแต่เริ่มต้นจนถึงเบราว์เซอร์เป้าหมาย

โดยรวมแล้ว Emscripten ทำงานได้ดี แต่ก็ค่อนข้างเกะกะ ฉันไม่ได้หมายถึงแค่วิพากษ์วิจารณ์มัน — ในฐานะนักอดิเรก ฉันรู้ว่า การทำสิ่งต่างๆ เป็นเรื่องยากมาก — แต่ให้สังเกตว่ามีพื้นที่ผิวอยู่ในนั้นจำนวนมากอย่างน่าประหลาดใจ เช่น API หลายตัวสำหรับการโต้ตอบกับ JavaScript และแฟล็กของคอมไพเลอร์ ด้วยชื่อเช่น EMULATE_FUNCTION_POINTER_CASTS มีการประชดว่า Emscripten เป็นเครื่องมือ JS อย่างไร แต่ ไลบรารี JS ของมันมีโครงสร้างเป็นกลุ่มของ globals

พอยน์เตอร์ Null นั้นน่าประหลาดใจ ในสภาพแวดล้อม C ทั่วไป dereference ตัวชี้ null ขัดข้อง ใน Wasm ตัวชี้ค่าว่างหมายถึง memory[0] และเป็นที่อยู่ทางกฎหมายสำหรับอ่านและเขียน รหัส C ที่อ่าน/เขียนตัวชี้ null เป็นสิ่งที่เราพยายามหลีกเลี่ยงอยู่แล้ว แต่ในสภาพแวดล้อม Wasm ตัวชี้ null รู้สึกเหมือน On Error Resume Next

สำหรับผู้ที่ไม่ใช่ผู้เชี่ยวชาญ ฉันสงสัยว่าคุณสามารถกู้คืน C semantics เกี่ยวกับการจัดการตัวชี้ null เมื่อรวบรวม C เป็น Wasm ได้หรือไม่ ถ้าคุณสามารถสร้างตัวชี้ null แทนที่อยู่หน่วยความจำบางตัวที่สูงถึงกับดัก ตัวอย่างเช่น ฉันจำได้ว่ามีกฎทางกฎหมายเกี่ยวกับ ภาษาว่า NULL ของ C เป็น 0 จริง หรือไม่ อีกทางหนึ่ง ฉันสงสัยว่าคุณสามารถเข้ารหัสทุกหน่วยความจำที่อ้างอิงถึงแทนการอ้างถึง theAddress-0x1000 ได้หรือไม่ โดยเปลี่ยนหน่วยความจำทั้งหมดลงอย่างมีประสิทธิภาพขณะรันไทม์ ซึ่งจะทำให้พอยน์เตอร์ null มีค่าต่ำกว่าที่อยู่สูงที่ผิดกฎหมายในทำนองเดียวกัน (ฉันคิดว่ามันอาจจะไม่ได้แพงเกินไปด้วยซ้ำ — คำแนะนำหน่วยความจำ Wasm ส่วนใหญ่ใช้พารามิเตอร์ออฟเซ็ตคงที่อยู่แล้ว…)

ความปลอดภัย. ในแง่หนึ่ง nullพอยน์เตอร์ที่ไม่ขัดข้องนั้นค่อนข้างแย่สำหรับการรัน C แต่ในอีกด้านหนึ่ง มันน่าสนใจที่จะพิจารณาว่า C ได้รับการสนับสนุนจากฮาร์ดแวร์และระบบปฏิบัติการที่ทันสมัยมากเพียงใด ไม่ใช่แค่หน้าป้องกันเพื่อทำให้ตัวชี้ null เกิดความผิดพลาด แต่ยังรวมถึง overcommit และหน้า W^X และ ASLR และ CFI เป็นต้น ฯลฯ ทั้งหมดในแง่หนึ่งเพื่อบรรเทาข้อบกพร่องที่แก้ไขโดยคอมไพเลอร์ / รันไทม์ที่อื่น (ตัวอย่างเช่น การจัดการค่า null ในทุกภาษาที่ไม่ใช่ C นั้นไม่ได้อาศัยความช่วยเหลือจาก CPU!)

สภาพแวดล้อม Wasm ไม่ได้รับการสนับสนุนแบบเดียวกัน ซึ่งหมายความว่าความปลอดภัยระดับภาษาอาจมีความสำคัญมากกว่าภายใต้ Wasm ในทางกลับกัน สถาปัตยกรรมฮาร์วาร์ดของ Wasm ยังไม่รวมการสร้างปัญหาของ C เช่น ROP; ยิ่งฉันได้ทำงานกับมันมากเท่าไหร่ ก็ยิ่งรู้สึกเหมือนเป็นตัวเลือกที่ “ถูกต้อง” มากขึ้นเท่านั้น และผลที่ตามมาของข้อบกพร่องก็อาจลดลงเช่นกัน เนื่องจากบัฟเฟอร์ล้นไม่ได้นำไปสู่การหลบหนีจากแซนด์บ็อกซ์ Wasm โดยตรง ต่อไปนี้คือการวิเคราะห์ที่ดีของบทความล่าสุดเกี่ยวกับความสมดุลระหว่างปัจจัยเหล่านี้ จากหนึ่งในบล็อกที่ฉันโปรดปราน

สำหรับฉันดูเหมือนว่าเราจะเห็น “การหลีกเลี่ยง Wasm sandbox เข้าสู่การประเมินเบราว์เซอร์” ของข้อบกพร่อง XSS ในอนาคตเพราะนั่นคือวิธีการรักษาความปลอดภัยที่ทำงานได้ทุกที่: สำหรับคุณสมบัติความปลอดภัยเฉพาะของ Wasm นั่นหมายความว่าบั๊กจะเป็น ที่ขอบเขตระหว่าง Wasm และระบบโฮสต์ แม้ว่า XSS ที่เกิดจาก Wasm จะเป็นการประนีประนอมที่แตกต่างจากการโจมตีตัวเบราว์เซอร์เองมาก แต่ XSS ที่ใช้ Wasm นั้นเทียบเท่ากับการกำหนดเป้าหมายไปยังโหนด nodejs สามารถเพิ่มการเรียกใช้โค้ดตามอำเภอใจได้อย่างง่ายดาย (ดูเอกสารด้านบน)

เค้าโครงหน่วยความจำ หน่วยความจำ Wasm เป็นบัฟเฟอร์แบนขนาดใหญ่ตัวหนึ่ง ในการแมป C กับสิ่งนี้ emscripten วางสแต็กไว้ที่ออฟเซ็ตคงที่และขยายลงมาและให้ฮีปเริ่มต้นที่จุดเดียวกันและเติบโตขึ้นไป หากคุณดู ไฟล์ C++ ในรูปแบบ weave คุณจะเห็นตัวชี้สแต็กเริ่มต้นที่กำหนดไว้ในส่วน “สากล” (บทความที่เชื่อมโยงจากโพสต์บล็อกที่เชื่อมโยงในส่วนก่อนหน้ามีเนื้อหามากกว่านี้)

เสมือน ใน โพสต์อื่นที่ ฉันพูดถึงกับดัก ซึ่งเป็นกรณีที่เครื่อง Wasm หยุดโปรแกรมของคุณ ที่ Figma เราพบกับดักในลักษณะที่น่าสนใจที่เกี่ยวข้องกับการโทรเสมือน

ใน C ++ กำหนด struct Iface { void foo(); } หากคุณเรียก ptr->foo() เมื่อ ptr เป็นโมฆะ จะไม่มีอะไรผิดพลาดในทันที มันเพิ่งเรียกใช้ foo ด้วย this == nullptr และ Dereferences เช่น this->someMember เพิ่งอ่านที่อยู่หน่วยความจำเหลือน้อยซึ่งทั้งหมดถูกกฎหมายใน Wasm

แต่ถ้า foo เป็นวิธีการ เสมือน สิ่งต่างๆ จะซับซ้อนมากขึ้น ดู ผลลัพธ์ที่สร้างขึ้นที่นี่ ตามความหมายของ C++ การโทรเสมือนจะค้นหา vtable ก่อนโดยยกเลิกการอ้างอิงตัวชี้ จากนั้นจึงรับที่อยู่ของฟังก์ชันเป้าหมายจากตารางนั้น (คุณสามารถเห็นสิ่งนี้ในเอาต์พุต godbolt โดยคู่ของคำสั่ง i32.load ) หากมีค่าว่างที่เกี่ยวข้องที่นี่ ก็ไม่มีปัญหาสำหรับ Wasm เพราะคุณเพิ่งได้รับดัชนีขยะกลับมา ในที่สุดก็มี Wasm opcode call_indirect ซึ่งเรียกใช้ฟังก์ชันที่เลือกโดยดัชนีคำนวณรันไทม์

หากดัชนีนั้นอ้างถึงฟังก์ชันที่อยู่นอกอาร์เรย์ฟังก์ชันที่โปรแกรมของคุณประกาศไว้ ฟังก์ชันนั้นจะดักจับ แต่ถ้าหลังจาก dereference nulls คุณบังเอิญอ้างถึงฟังก์ชันที่มีอยู่บางฟังก์ชัน ในเอาต์พุตด้านบน ให้สังเกตว่า ประเภท ของฟังก์ชันเป้าหมายที่คาดไว้นั้นรวมอยู่ในคำสั่ง call_indirect อย่างไร

เหตุผลที่ประเภทฟังก์ชันปรากฏขึ้นในความหมายของ Wasm เป็นบางอย่างเกี่ยวกับคุณสมบัติความสมบูรณ์ของ Wasm โดยที่ฉันเข้าใจ มันจำเป็นต้องรู้ว่าค่าประเภทใดที่อยู่ในสแต็ก ณ จุดใดก็ได้ แต่ความหมายในกรณีนี้คือคุณมีดัชนีฟังก์ชันขยะ และหากดัชนีขยะนั้นอ้างอิงถึงฟังก์ชันที่มีประเภทแตกต่างจากที่คุณคาดไว้ Wasm จะดักจับ

เพื่อสรุปส่วนนี้: nullพอยน์เตอร์ไม่ขัดข้องโดยทั่วไป แต่พอยน์เตอร์ null ที่เสมือนสามารถเกิดขึ้นได้ หากบังเอิญอ้างถึงฟังก์ชันที่มีประเภทระดับ Wasm อื่นโดยไม่ได้ตั้งใจ สิ่งที่น่าสยดสยองเกี่ยวกับเรื่องนี้คือคุณสามารถมีโค้ดบางส่วนที่เกี่ยวข้องกับตัวชี้ null สามารถเขียนลวก ๆ ทั่วทั้งหน่วยความจำได้อย่างมีความสุขและทำงานต่อไปได้และวิธีเดียวที่คุณจะสังเกตเห็นได้ก็คือถ้าในที่สุดมันจะดักจับในการโทรเสมือน

บัฟเฟอร์ทางอ้อม รูปแบบที่น่าสนใจอย่างหนึ่งที่เราไว้วางใจที่ Figma เรียกว่า IndirectBuffer แนวคิดพื้นฐานคือคุณสามารถจัดสรรอาร์เรย์ใน JavaScript และยังคงจัดการจาก C ++ โดยเปิดเผยการเรียกใช้แม้ว่าไบต์ของอาร์เรย์จะไม่เคยอยู่ในหน่วยความจำ C ++ สิ่งนี้มีความสำคัญเนื่องจากหน่วยความจำ Wasm ถูก จำกัด ไว้ที่ ~ 4gb แต่หน่วยความจำ JS ไม่ใช่

โดยเฉพาะอย่างยิ่ง เอกสาร Figma จัดการกับรูปภาพจำนวนมาก ดังนั้นเราจึงพยายามเก็บพิกเซลของรูปภาพไว้ในหน่วยความจำ JS หรือ GPU ในขณะที่ยังคงแสดงฉากจาก C++ ตัวอย่างหนึ่งของสิ่งนี้ โปรเจ็กต์หนึ่งที่ฉันทำงานอยู่ที่ Figma เกี่ยวข้องกับฟังก์ชัน “บันทึกเอกสารปัจจุบันเป็นไฟล์” ซึ่งจำเป็นต้องจัดลำดับเนื้อหาทั้งหมดของเอกสารปัจจุบัน (รวมถึงพิกเซลของรูปภาพ) ลงในบัฟเฟอร์ขนาดใหญ่เพียงอันเดียว เอกสารที่โหลดนั้นกินพื้นที่ Wasm ส่วนใหญ่ของคุณไปแล้ว ดังนั้นเราจึงย้ายเอาต์พุตการทำให้เป็นอนุกรมจำนวนมากไปยังบัฟเฟอร์ระดับ JS แม้ว่ารหัสการทำให้เป็นอนุกรมทั้งหมดยังคงอยู่ใน C++

ตอนเป็นเด็ก ฉันได้เริ่มเขียนโปรแกรมในยุค DOS และฉันยังจำการลงทะเบียนเซกเมนต์และพ อยน์เตอร์ที่อยู่ห่างไกล ได้ ทั้งหมดนี้ทำให้ฉันมีความทรงจำที่สนุกสนานในช่วงเวลานั้น แต่ปัญหานี้อาจค่อนข้างเฉพาะเจาะจงสำหรับ Figma ซึ่งจะกิน RAM ทั้งหมดที่คุณให้ได้อย่างมีความสุข

ยังเช้าอยู่ งานกำลังดำเนินการเพื่อขอข้อมูลการดีบัก DWARF ลงใน devtools ของเบราว์เซอร์ ดูเหมือนว่าจะมีแนวโน้มดี แต่เครื่องมือยังค่อนข้างเร็ว มันอยู่นอกขอบเขตสำหรับโพสต์นี้ แต่ Figma มีการตั้งค่าที่น่าทึ่ง/ผิดพลาดที่เราสามารถสร้างและดีบักส่วน C ++ ของแอปโดยใช้ toolchain ดั้งเดิมแม้ว่าแอปส่วนใหญ่จะถูกเขียนด้วย HTML ซึ่งส่วนใหญ่เรายังคงมีชีวิตอยู่เพราะประสบการณ์การพัฒนาดั้งเดิมคือ ยังดีขึ้นมาก

นอกจากนั้น หากคุณมี C++ call stack ที่เรียกใช้ JS แล้วเรียก new Error() วัตถุที่ได้จะมีเฟรม C++ อยู่ และมีเครื่องจักรเพียงพอที่คุณจะได้รับสัญลักษณ์ ฉันพูดถึงการดีบักเป็นส่วนใหญ่เพื่อบอกว่ามีการปรับปรุงอย่างรวดเร็ว (มันค่อนข้างเรียบร้อยสำหรับขั้นตอนเดียวในเบราว์เซอร์ผ่าน C++ stack!) และยังเชื่อมโยงคุณ กับโพสต์บล็อกอื่นอย่างละเอียด อีกด้วย

อีกตัวอย่างหนึ่ง ใน emscripten toolchain สแต็คของเครื่องมือที่ใช้ในการเชื่อมโยงคือ Wasm/emscripten-specific ซึ่งหมายความว่าพวกเขาไม่เคยเห็นเวลาหลายปีของการปรับแต่งเพื่อให้ทั้งสองทำงานได้อย่างรวดเร็วและสร้างโค้ดที่แน่นหนาที่คุณได้รับบนแพลตฟอร์มอื่น (โดยเฉพาะ Linux) .

อีกตัวอย่างหนึ่ง “มันเร็ว” แม้ว่าฉันเดาว่าตอนนี้ฉันอยู่ในหัวข้อ ฉันรู้ว่าสิ่งนี้ไม่มีอะไรพิเศษเกี่ยวกับ C++: เราเพิ่งพบจุดบกพร่องในการสร้างโค้ดในตัวเพิ่มประสิทธิภาพ Wasm ของ Firefox ฉันยังค่อนข้างแปลกใจที่เพื่อนร่วมงานของฉันสามารถกลั่นกรองข้อผิดพลาดรันไทม์ของ Figma ให้กลายเป็น repro ขนาดเล็กได้ และประทับใจเพิ่มเติมว่าวิศวกรของ Firefox สามารถเขียนโปรแกรมแก้ไขได้ 90 นาทีหลังจากรายงาน (?!)

โดยรวมแล้ว การพัฒนา C++ บน Wasm ให้ความรู้สึกเหมือนกับที่ฉันจินตนาการว่าการพัฒนา C++ สำหรับบางสิ่งเช่นระบบฝังตัวอาจเป็นเช่น: ยังคงเป็น C++ อย่างแน่นอน แต่เครื่องมือนั้นอ่อนแอกว่าเล็กน้อยและแตกต่างไปจากเครื่องมือที่คุณคุ้นเคย

ชะตากรรมของ C++ ในช่วงฤดูร้อนนี้ เป็นเวลา 25 ปีแล้วที่ฉันมีงานเขียนโปรแกรมครั้งแรก เขียน C++ สำหรับการเริ่มต้นดอทคอม และวันนี้ฉันยังคงเขียน C++ สำหรับการเริ่มต้นดอทคอม จากมุมมองนั้น การเรียนรู้ C++ เป็นการลงทุนที่ชาญฉลาด แต่ฉันคิดว่าหลายคนที่มีประสบการณ์ C ++ คล้ายคลึงกันจะเห็นด้วยกับฉันว่ามันค่อนข้างหายากที่จะเป็นเครื่องมือที่เหมาะสมสำหรับซอฟต์แวร์ในปัจจุบัน

โดยเฉพาะอย่างยิ่ง ฉันคิดว่า C ++ เป็นเครื่องมือระดับผู้เชี่ยวชาญในแง่ที่ไม่ดี ซึ่งหากคุณทำผิดพลาด มันก็จะล้มเหลวอย่างมีเจตนาร้าย: ด้วยความเสียหายของหน่วยความจำแบบเงียบหรือประสิทธิภาพที่ไม่ดีอย่างเงียบๆ เนื่องจากการคัดลอกโดยไม่ได้ตั้งใจ ในขณะเดียวกัน ดังที่ Zaplib post-mortem ได้ค้นพบ มันไม่ชัดเจนว่าภาษาที่มีประสิทธิภาพดีกว่านั้นแปลเป็นประสิทธิภาพได้อย่างง่ายดาย และหากไม่ใช่เพื่อประสิทธิภาพ ก็ยังมีเหตุผลน้อยกว่าในการเขียน C++

ด้วยเหตุนี้ ฉันจึงเห็นอนาคตของ C++ และ Wasm อยู่ที่การรวมโค้ด C++ แบบเก่าเป็นส่วนใหญ่ ไม่ใช่การเขียนโค้ดใหม่ ส่วนใหญ่เป็นเพียงคำแถลงเกี่ยวกับความรู้สึกของฉันเกี่ยวกับ C ++ โดยทั่วไป แต่โดยเฉพาะอย่างยิ่ง Wasm ทำให้ชีวิตใหม่สามารถเชื่อมโยงรหัส C ++ เก่าเข้ากับบริบทใหม่ได้ ตัวอย่างเช่น Mozilla ส่ง แฮ็คที่น่าทึ่ง ซึ่งใช้ Wasm กับโค้ด C++ แบบแซนด์บ็อกซ์ที่ใช้ใน Firefox เอง: พวกเขารวบรวม C++ ของตนไปยัง Wasm แล้วเชื่อมโยงกลับเข้าไปในแอป C ++ ที่ใหญ่กว่า โดยมีเอฟเฟกต์รันไทม์เกือบจะเหมือนกับการเรียกใช้โมดูลย่อย C++ ภายใน โปรแกรมจำลอง

หมายเหตุเกี่ยวกับ WebAssembly

ตอนนี้ฉันใช้เวลาประมาณหนึ่งปีในการทำงานกับ WebAssembly เหมือนกับ บันทึกย่อของฉันเมื่อฉันก้าวเข้าสู่ TypeScript เป็นครั้งแรก นี่คือสิ่งที่ “ผู้เริ่มต้นขั้นสูง” บางส่วนที่ฉันได้เรียนรู้เกี่ยวกับ WebAssembly โดยเน้นที่เบราว์เซอร์และ C++ โดยเฉพาะ

หากคุณเป็นคนประเภทที่ชอบแหย่ตัวอย่าง ฉันได้สร้างโปรแกรมดูไฟล์ WebAssembly ชื่อ “สาน” ที่ให้คุณสำรวจเนื้อหาของไฟล์ . .wasm แบบโต้ตอบได้ คุณสามารถเล่นกับไฟล์สาธิตบางส่วนได้ที่นี่

โดยเฉพาะอย่างยิ่ง โปรดทราบว่าคุณสามารถคลิกเข้าไปในส่วน “โค้ด” และจากส่วนนั้นไปยังเนื้อหาฟังก์ชัน และจากนั้นยังติดป้ายกำกับพารามิเตอร์/โลคัลที่ไม่ได้ติดป้ายกำกับไว้เพื่อช่วยในการอ่านโค้ด

สิ่งหนึ่งที่ดึงดูดใจฉันให้มาทำงานที่ Figma คือโอกาสที่จะได้เรียนรู้และทำงานกับ WebAssembly ปรากฎว่า Figma เป็นสภาพแวดล้อมที่ดีสำหรับสิ่งนี้ Figma เป็นการผสมผสานระหว่าง “ดั้งเดิมกับเว็บ” ซึ่งเป็นแอปจำนวนมากที่เขียนในสแต็ก React/TypeScript ทั่วไป ขณะเดียวกันก็ใช้โค้ด C++ ระดับต่ำในปริมาณที่น่าประหลาดใจ เอกสาร Figma ที่แก้ไขในเบราว์เซอร์ของคุณแสดงผลโดยใช้ GPU shaders ที่จัดการโดยกราฟฉาก C ++ ซึ่งคุณอาจสังเกตเห็นได้ก็ต่อเมื่อคุณคิดว่าการซูมจะราบรื่นเพียงใด! กระทู้นี้ขอแทรกนิดนึง

บทนำหนึ่งย่อหน้า WebAssembly (“Wasm”) เป็นรูปแบบคำสั่ง + เครื่องเสมือนที่ตอนนี้มีอยู่ในเบราว์เซอร์ ได้รับการออกแบบมาเพื่อให้สามารถรันโค้ดจากภาษาอื่นที่ไม่ใช่ JavaScript บนเว็บได้อย่างปลอดภัย แต่มีแอพพลิเคชั่นอื่น ๆ ที่หลากหลาย ในแง่นามธรรม เป็นแนวคิดเดียวกันกับ Native Client , JVM หรือ CLR ไม่มากก็น้อย แม้ว่าจะแตกต่างกันในหลาย ๆ ด้านก็ตาม มีแอปพลิเคชั่นที่น่าสนใจของ Wasm อยู่นอกเบราว์เซอร์ แต่ฉันจะปล่อยให้พวกเขาไม่อยู่ในขอบเขตสำหรับโพสต์นี้

มันทำงานอย่างไร? มีรูปแบบการทำให้เป็นอนุกรมและ bytecode เป็นต้น แต่วิธีดูอีกวิธีหนึ่งก็คือมันทำงานค่อนข้างคล้ายกับวิธีที่ JS ดำเนินการ: แซนด์บ็อกซ์จากระบบปฏิบัติการ โดยทั่วไปแล้วคอมไพเลอร์ที่เพิ่มประสิทธิภาพจะประมวลผลล่วงหน้าสำหรับโค้ดที่คุณจัดส่ง ในมุมมองนั้น เมื่อเทียบกับ JS จะทำการดีซีเรียลไลซ์ได้เร็วกว่าและเพิ่มประสิทธิภาพได้ง่ายกว่า ความแตกต่างที่สำคัญอื่น ๆ จาก JS คือไม่มีไลบรารีมาตรฐานหรือ API ของเบราว์เซอร์

ข้อมูลจำเพาะ ข้อมูลจำเพาะของ Wasm เป็นความสุขที่แท้จริง รวบรัดและเฉพาะเจาะจง มันทำให้หัวใจของผู้ที่ชื่นชอบ PL อบอุ่น ในการเขียนเครื่องมืออย่าง weave ฉันพบว่ารูปแบบค่อนข้างเหมาะสมที่จะใช้งานด้วย

สถาปัตยกรรมที่เรียบง่าย เครื่อง Wasm (ฉันคิดว่า?) ง่ายกว่าระบบที่เทียบเคียงได้มาก มันเป็นเครื่องสแต็คไม่มีการลงทะเบียน คำแนะนำโดยทั่วไปจะผลักและป๊อปอิน 32/64 บิตและลอยบนสแต็กนั้น รหัสคืออาร์เรย์ของฟังก์ชัน ซึ่งอาจเรียกกันโดยดัชนีอาร์เรย์ สภาพแวดล้อมการโฮสต์สามารถลงทะเบียนฟังก์ชันในอาร์เรย์นั้นได้เช่นกัน เพื่อให้โค้ด Wasm เรียกออกมา หน่วยความจำเป็นอาร์เรย์แบบแบนที่จัดทำดัชนีโดยที่อยู่แบบ 32 บิต หน่วยความจำ สแต็กการโทร และโค้ดทั้งหมดแยกจากกัน — “สถาปัตยกรรมฮาร์วาร์ด”

ยิ่งไปกว่านั้น ไม่มีแนวคิดที่ชัดเจนเกี่ยวกับอ็อบเจ็กต์ การรวบรวมขยะ พอยน์เตอร์ สตริง อาร์เรย์ โมดูล ฯลฯ ฟังก์ชัน Wasm ของคุณสามารถเรียกกันและกัน คิดเลข อ่านและเขียนหน่วยความจำ และมันก็เพียงพอแล้ว

รูปแบบข้อความ ข้อมูลจำเพาะกำหนด รูปแบบข้อความ ที่เป็น 1:1 ด้วย bytecode ซึ่งแสดงผลโดยใช้ s-expressions Godbolt (หรือที่รู้จักในชื่อ “Compiler Explorer”) ยังให้คุณดูเอาต์พุต Wasm ที่สร้าง Clang; นี่คือฟังก์ชัน “เพิ่ม” ง่ายๆ น่าเสียดายที่ผลลัพธ์ไม่ใช่รูปแบบข้อความ แต่เป็นสิ่งที่ฉันคิดว่าสร้างขึ้นโดย Clang แต่ถ้าคุณคุ้นเคยกับ godbolt อยู่แล้ว ก็คงไม่มีปัญหาอะไรกับเรื่องนี้มากนัก (Weave โปรแกรมดู Wasm ของฉันที่ลิงก์ด้านบน ยังแสดงคำแนะนำในลักษณะที่ไม่เป็นมาตรฐาน เนื่องจากเน้นที่การนำเสนอข้อความของเนื้อหาไฟล์ต้นฉบับ)

ประสิทธิภาพการทำงาน WebAssembly ประสิทธิภาพมายากลฝุ่น pixie หรือไม่? (คำตอบสั้น ๆ : ไม่) เป็นคำถามที่ดีและมีรายละเอียดเกี่ยวกับประสิทธิภาพ วิธีตอบคำถามเดียวกันในระดับที่สูงกว่าก็คือ ในงานด้านประสิทธิภาพ โดยปกติ การเปลี่ยนแปลงทางสถาปัตยกรรมมีความสำคัญมากกว่าการเพิ่มประสิทธิภาพลูป จากที่กล่าวมา Wasm ให้การควบคุมระดับต่ำแก่คุณ ดังนั้นเมื่อเทียบกับ JS มันค่อนข้างเหมาะมากสำหรับการรันโค้ดที่คุณสนใจเกี่ยวกับสิ่งต่างๆ เช่น ประสิทธิภาพของเลย์เอาต์หน่วยความจำ และเป็นเป้าหมายการรวบรวมที่ง่ายกว่าสำหรับภาษาอื่น

พอยน์เตอร์ หน่วยความจำ Wasm ถูกจำกัดไว้ที่ 4gb ซึ่งหมายความว่าพอยน์เตอร์มีขนาดเพียง 4 ไบต์ ในขณะเดียวกัน v8 บนแพลตฟอร์ม 64 บิตใช้พอยน์เตอร์ 64 บิต ซึ่งหมายความว่าต้องใช้ ความพยายามอย่างมากเพื่อไม่ให้พอยน์เตอร์ 8 ไบต์กินหน่วยความจำทั้งหมดของคุณ (ฉันพบบล็อกโพสต์นี้ “Handles are the better pointers” เปลี่ยนมุมมองของฉันในด้านนี้จริงๆ คุ้มค่ากับเวลาของคุณ ที่เกี่ยวข้อง ใน การทดลอง n2 ของ ฉัน ฉันตระหนักว่าการจำกัดให้เหลือ “เพียง 4 พันล้านไฟล์ในแต่ละครั้งเท่านั้น” ลดขนาดที่จัดการกราฟทั้งหมดลงครึ่งหนึ่งโดย เปลี่ยนเป็น 32 บิต )

หมายเหตุด้านที่น่าขบขัน: Wasm ถูก จำกัด ไว้ที่ 4gb แต่ไฟล์ที่กำหนดสามารถประกาศขีด จำกัด ล่าง ซึ่งหมายความว่าบางรหัสต้องแสดงตัวเลข “ขีด จำกัด หน่วยความจำปัจจุบัน” ตัวเลขนี้ต้องใช้จำนวนเต็ม 64 บิตเพื่อเป็นตัวแทน เนื่องจากจำนวนเต็ม 32 บิตที่ไม่ได้ลงนามจะสูงถึง 2**32-1 เท่านั้น ซึ่งน้อยกว่าค่าสูงสุดที่เป็นไปได้หนึ่งค่า! Chrome ทำผิดและมีข้อบกพร่องในพื้นที่นี้ ซึ่งหมายความว่าในการทดลองปัจจุบันของ Figma โดยใช้ขีดจำกัด 4GB เราจำกัดไว้ที่ 4GB ลบหนึ่งหน้าเพื่อหลีกเลี่ยงข้อบกพร่องเหล่านั้น

ความปลอดภัย. Wasm อยู่ในแซนด์บ็อกซ์ แต่คุณสามารถเรียกใช้ C ++ ได้ มันทำงานอย่างไร? ข้อมูลจำเพาะมี ทั้งส่วนที่เกี่ยวกับ ความสมบูรณ์ที่สร้างความปลอดภัยของหน่วยความจำในความหมายของ Wasm แต่จริงๆ แล้วไม่ได้ตอบคำถามด้านความปลอดภัยที่คุณอาจมี

นี่คือสิ่งที่ชัดเจนสำหรับฉันในตอนนี้ แต่ฉันคิดว่าไม่ชัดเจนเมื่อฉันเริ่ม C++ มีแนวคิดเกี่ยวกับ stack และ heap ของตัวเอง โดยใน C++ คุณสามารถระบุที่อยู่ของบางสิ่งใน stack ได้ สิ่งนี้แปลเป็น Wasm โดยตัวแปรสแต็ก C++ ที่มีอยู่ในหน่วยความจำ Wasm-addressable ในลักษณะเดียวกับที่สแต็ก C ++ มีอยู่ในหน่วยความจำ หน่วยความจำนั้นอยู่ภายใต้การควบคุมของรหัส C ++ โดยสิ้นเชิง ในขณะเดียวกัน โครงสร้างรันไทม์ของ Wasm รวมถึง call stack นั้นแยกจากกันและ C++ จะมองไม่เห็นอย่างมีประสิทธิภาพ โดยสรุป ข้อผิดพลาดของหน่วยความจำ C++ เช่น บัฟเฟอร์ล้นยังคงสามารถกระทืบบนหน่วยความจำ C++ ได้ แต่หน่วยความจำนั้นแตกต่างจากโครงสร้าง Wasm ใดๆ เช่น สแต็กการโทร ดังนั้นโปรแกรม Wasm ที่เสียหายทั้งหมดสามารถทำได้คือทำให้หน่วยความจำของตัวเองยุ่งเหยิง ไม่ใช่ของโฮสต์

หนีจาวาสคริปต์ Wasm หมายความว่าตอนนี้คุณสามารถเขียนภาษาที่คุณชื่นชอบแทน JavaScript หรือเรียกใช้โปรแกรมโปรดของคุณในเบราว์เซอร์ได้หรือไม่? ประมาณว่าแต่ไม่ใช่เลย เป็นความจริงที่เป็นไปได้ที่จะคอมไพล์สิ่งต่าง ๆ ไปยัง Wasm แต่ก็สามารถคอมไพล์หลายสิ่งไปยัง JS ได้แล้ว กล่าวอีกนัยหนึ่ง Wasm ทำให้สิ่งต่าง ๆ มีประสิทธิภาพมากขึ้น แต่ไม่เปิดเผยผลลัพธ์หรือพฤติกรรมทางความหมายใหม่ใด ๆ นอกเหนือจากผลปกติของสิ่งต่าง ๆ ที่เร็วขึ้น

สาเหตุที่ไม่ใช่ JS ในเบราว์เซอร์ไม่ได้ผลมักจะเหมือนกันหมดไม่ว่าคุณจะกำหนดเป้าหมาย JS หรือ Wasm ด้วยปรากฏการณ์ที่ฉันชอบเรียก Probst’s paradox : ซอฟต์แวร์และภาษาโปรแกรมที่ไม่ได้เขียนให้ทำงานบนเว็บเพื่อเริ่มต้น ด้วยจะมีสมมติฐานของตนเองเกี่ยวกับสภาพแวดล้อมที่ทำให้พวกเขาทำงานบนเว็บได้ไม่ดี สำหรับหลายภาษา ข้อสันนิษฐานคือรันไทม์หรือไลบรารีของภาษาขนาดใหญ่หรือตัวรวบรวมขยะมีให้ใช้ฟรี และสำหรับหลายๆ โปรแกรม ข้อสันนิษฐานคือมีสิ่งต่างๆ เช่น “ไฟล์” หรือ “หน้าจอ” หรือ “stdout” แน่นอน เกือบทุกอย่างสามารถสร้างขึ้นเพื่อใช้ค้อนทุบได้เพียงพอ

(คุณสามารถเห็นไดนามิกที่คล้ายคลึงกันเมื่อมีคนสร้างพอร์ตของเว็บแอพไปยังโทรศัพท์ที่แย่ ๆ คุณสามารถสร้างแอพที่ดีโดยใช้เทคโนโลยีเว็บบนโทรศัพท์ – ตัวอย่างเช่น Libby นั้นยอดเยี่ยมมาก! แต่ถ้าคุณต้องการนำงานที่คุณใส่กลับมาใช้ใหม่ ในการสร้างเว็บแอปเก่าโดยใช้โทรศัพท์ ผลลัพธ์ที่ได้ตามปกตินั้นไม่ค่อยดีนัก เนื่องจากเว็บแอปไม่ได้เขียนขึ้นโดยคำนึงถึงโทรศัพท์เป็นหลัก)

เป็นเรื่องที่น่าสนใจสำหรับฉันที่ C (แตกต่างจากเช่น Go หรือ Python เป็นต้น) กลายเป็นหนึ่งในภาษาที่ดีกว่าสำหรับการกำหนดเป้าหมาย Wasm ในแง่นี้เนื่องจาก C ได้รับการออกแบบมาเพื่อทำงานในสถานที่ที่มีรันไทม์ไม่มากนัก (เช่น no_std ใน Rust) แต่โปรดทราบว่าถึงแม้การเรียกใช้ C ภายใต้ Wasm คุณจะต้องส่งการนำ malloc ไปใช้งานในชุด Wasm ของคุณ

สภาพแวดล้อมการโฮสต์ เมื่อทำงานในเบราว์เซอร์ หน่วยความจำ Wasm เป็นเพียง ArrayBuffer ภายในรหัส Wasm สามารถเขียนอะไรก็ได้ตามต้องการในหน่วยความจำนั้น ในการเรียกสภาพแวดล้อม (เช่นเบราว์เซอร์) สามารถเรียกใช้ฟังก์ชันที่เปิดเผยได้ แต่ Wasm เท่านั้นที่รู้คือตัวเลข

นี่หมายถึงการส่งสตริงหรือโครงสร้างออกไป คุณส่งที่อยู่ และจากด้าน JS คุณเขียน let x = memory[address]; เพื่ออ่านไบต์จากหน่วยความจำ (นั่นไม่ใช่ pseudocode ด้วยซ้ำ ดู ที่ Memory.buffer !) มีหลายวิธีที่จะรวมสิ่งเหล่านี้เข้าด้วยกัน แต่ทั้งหมดนั้นเกี่ยวข้องกับต้นทุนและการคัดลอกบ่อยครั้ง ตัวอย่างเช่น ในการแปลงสตริง Wasm เป็นออบเจ็กต์ JS String ซึ่งคุณต้องดำเนินการเพื่อเรียกใช้ API ของเบราว์เซอร์ที่เกี่ยวข้องกับสตริง คุณต้องส่งส่วนของไบต์อาร์เรย์หน่วยความจำผ่าน TextDecoder หรือสิ่งที่เทียบเท่า นี้ไม่ฟรี

ตามทฤษฎีแล้ว คุณสามารถมี JS call Wasm call JS และ thread call stacks ผ่านภาษาใดก็ได้ แต่ขอบเขตระหว่างทำให้มันมีราคาแพงกว่าที่คุณต้องการ และ Wasm ไม่ได้ทำให้คุณสามารถผสมภาษาที่ไม่ใช่ JS สองภาษาได้อย่างน่าอัศจรรย์ มากไปกว่าที่คุณสามารถผสมภาษาเหล่านั้นนอก Wasm ได้

ฉันจะเน้นที่นี่ว่า Wasm ในเบราว์เซอร์คือ Wasm ทำงานพร้อมกันในเธรดเดียวกันกับ JS ไม่ใช่ในฐานะผู้ปฏิบัติงานที่แยกจากกัน เมื่อดวงดาวเรียงกันเพื่อให้ devtools ของคุณทำงาน หมายความว่าเช่น การติดตามสแต็กสามารถติดตามย้อนหลังผ่านเฟรม JS และ Wasm ในสแต็กเดียวกัน (แน่นอนว่าเป็นไปได้ที่จะเรียกใช้ Wasm ในคนงานด้วย)

พังได้ไหม โดยทั่วไปแล้ว โค้ดของ Wasm สามารถทำอะไรก็ได้ตามต้องการในหน่วยความจำของตัวเอง ดังนั้นปัญหาระดับภาษา ซึ่งรวมถึง “การละเลยตัวชี้ค่า null” ถือเป็นเรื่องถูกกฎหมาย การเขียนไปยังตัวชี้ค่าว่างเป็นเพียงการเขียนไปยังอาร์เรย์หน่วยความจำที่ศูนย์ออฟเซ็ต ซึ่งถูกกฎหมายเหมือนกับการเขียนอื่นๆ

แต่โค้ดที่ไม่ถูกต้องใน Wasm ยังคงทำงานผิดพลาดได้ในขณะใช้งานจริง! ในสเปกเขาเรียกว่า กับดัก สำหรับตัวอย่างง่ายๆ ของกับดัก ลองนึกภาพโค้ดที่เข้าถึง memory[n] สำหรับบาง n ที่ใหญ่กว่าขนาดหน่วยความจำของคุณ คุณสามารถดู กับดักที่ระบุได้ที่นี่

อย่างไรก็ตาม Wasm มี ” การตรวจสอบความถูกต้อง ” ผ่านเมื่อโหลดซึ่งยืนยันคุณสมบัติหลายอย่าง เช่น “การเรียกใช้ฟังก์ชันโดยตรงทั้งหมดอ้างอิงถึงดัชนีฟังก์ชันที่ถูกต้อง” ดังนั้น พฤติกรรมที่อาจไม่ถูกต้องหลายประเภทเหล่านั้นจึงถูกคัดออกในขณะโหลด การตรวจสอบความถูกต้องนี้เป็นส่วนหนึ่งของสาเหตุที่ Wasm สามารถดำเนินการได้อย่างมีประสิทธิภาพขณะใช้งานจริงในขณะที่ยังปลอดภัยอยู่ สำหรับตัวอย่างอื่น การกระโดดใน Wasm มีโครงสร้างในลักษณะที่น่าสนใจ โดยจะอ้างอิงถึงขอบเขตของบล็อกเสมอ

ที่ Figma เราพบกับดักในลักษณะที่ละเอียดอ่อนซึ่งเกี่ยวข้องกับการโทรเสมือน C++ แต่ฉันมีอะไรอีกมากที่จะพูดเกี่ยวกับ C++ และ Wasm โดยเฉพาะอย่างยิ่ง ดังนั้นฉันจึงวางแผนที่จะพูดถึงทั้งหมดนั้นในโพสต์อื่น