7 คะแนน โดย GN⁺ 2025-05-10 | 3 ความคิดเห็น | แชร์ทาง WhatsApp
  • แม้ว่า ระบบจัดการการพึ่งพา ของ Rust จะช่วยให้การพัฒนาสะดวกขึ้น แต่จำนวนและคุณภาพของ dependency ก็กลายเป็นเรื่องที่น่ากังวล
  • แม้แต่ Crate ที่ใช้งานกันอย่างแพร่หลายก็อาจไม่ได้อัปเดตอยู่เสมอไป จนบางครั้งการเขียนเองอาจดีกว่า
  • หลังเพิ่ม Crate ชื่อดังอย่าง Axum, Tokio เป็นต้น จำนวน บรรทัดโค้ด รวมทั้งหมดเมื่อรวม dependency แล้วพุ่งไปถึง 3.6 ล้านบรรทัด จนยากจะรับมือ
  • โค้ดที่ผู้เขียนเขียนจริงมีเพียงราว 1,000 บรรทัดเท่านั้น แต่โค้ดรอบข้างแทบเป็นไปไม่ได้ที่จะตรวจทานและ audit
  • ยังไม่มี ทางออก ที่ชัดเจนว่าควรขยาย standard library ของ Rust หรือจัดการโครงสร้างพื้นฐานหลักอย่างไร และทั้งชุมชนก็ต้องช่วยกันคิดหาสมดุลระหว่างประสิทธิภาพ ความปลอดภัย และการดูแลรักษา

ภาพรวมของปัญหาการพึ่งพาใน Rust

  • Rust เป็นภาษาที่ผู้เขียนชอบที่สุด และทั้ง ชุมชนกับประสบการณ์การใช้งานภาษา ก็ยอดเยี่ยมมาก
  • แม้ประสิทธิภาพในการพัฒนาจะสูง แต่ช่วงหลังเริ่มกังวลในด้าน การจัดการ dependency

ข้อดีของ Rust Crate และ Cargo

  • Cargo ช่วย จัดการแพ็กเกจและทำงาน build อัตโนมัติ ได้ ทำให้ประสิทธิภาพการพัฒนาดีขึ้นมาก
  • ย้ายข้ามระบบปฏิบัติการและสถาปัตยกรรมต่าง ๆ ได้ง่าย และไม่ต้องกังวลเรื่อง จัดการไฟล์เองหรือปรับตั้งค่าเครื่องมือ build
  • สามารถเริ่ม เขียนโค้ด ได้ทันทีโดยไม่ต้องกังวลเรื่องการจัดการแพ็กเกจเพิ่มเติม

ข้อเสียของการจัดการ Crate ใน Rust

  • เมื่อไม่ต้องใส่ใจกับการจัดการแพ็กเกจมากนัก ก็ทำให้ละเลยเรื่อง ความเสถียร ได้ง่าย
  • ตัวอย่างเช่น ผู้เขียนเคยใช้ crate dotenv แล้วมาทราบผ่าน Security Advisory ว่าหยุดบำรุงรักษาแล้ว
  • ระหว่างพิจารณา crate ทางเลือก (dotenvy) ก็พบว่าส่วนที่ต้องใช้จริงเขียนเองได้ในประมาณ 35 บรรทัด
  • ปัญหาแพ็กเกจขาดการดูแลเกิดขึ้นบ่อยในหลายภาษา ดังนั้นแก่นของปัญหาคือ สถานการณ์ที่หลีกเลี่ยง dependency ไม่ได้

ปริมาณโค้ดที่พุ่งสูงจาก dependency

  • ผู้เขียนใช้งาน แพ็กเกจสำคัญและมีคุณภาพดีใน ecosystem ของ Rust อย่าง Tokio และ Axum อยู่
  • ได้เพิ่ม Axum, Reqwest, ripunzip, serde, serde_json, tokio, tower-http, tracing และ tracing-subscriber เป็น dependency
  • จุดประสงค์หลักมีแค่เว็บเซิร์ฟเวอร์ การแตกไฟล์บีบอัด และระบบล็อก จึงถือว่า ตัวโปรเจกต์เองค่อนข้างเรียบง่าย
  • ใช้ความสามารถ Cargo vendor เพื่อ ดาวน์โหลด crate ที่พึ่งพาทั้งหมดมาไว้ในเครื่อง
  • เมื่อลองวิเคราะห์จำนวนบรรทัดโค้ดด้วย tokei พบว่ารวม dependency แล้วมี ประมาณ 3.6 ล้านบรรทัด (ถ้าไม่รวม crate ที่ vendor มา จะเหลือประมาณ 11,136 บรรทัด)
  • เพื่อเทียบให้เห็นภาพ เคยมีข้อมูลว่า Linux kernel ทั้งหมดมีประมาณ 27.8 ล้านบรรทัด ดังนั้นโปรเจกต์เล็ก ๆ นี้จึงมีขนาดราวหนึ่งในเจ็ดของนั้น
  • โค้ดที่ผู้เขียนเขียนเองจริง ๆ มีเพียงประมาณ 1,000 บรรทัด
  • การเฝ้าดูและ audit โค้ด dependency จำนวนมหาศาลเช่นนี้ แทบเป็นไปไม่ได้ในทางปฏิบัติ

ความกังวลเรื่องทางออก

  • ตอนนี้ยังไม่มีทางออกที่ชัดเจน
  • บางส่วนเสนอให้ ขยาย standard library แบบ Go แต่แนวทางนี้ก็สร้างปัญหาใหม่ เช่น ภาระในการดูแลรักษา
  • Rust มุ่งเน้น ประสิทธิภาพสูง ความปลอดภัย และความเป็นโมดูลาร์ รวมถึงตั้งเป้าแข่งขันในงาน embedded และ C++ จึงต้องระมัดระวังเรื่องการขยาย standard library
  • ตัวอย่างเช่น runtime ขั้นสูงอย่าง Tokio ก็ยังได้รับการดูแลอย่างคึกคักมากบน Github และ Discord
  • ในความเป็นจริง การ ลงมือสร้าง runtime แบบ asynchronous หรือเว็บเซิร์ฟเวอร์ซึ่งเป็นโครงสร้างพื้นฐานหลักด้วยตัวเอง เป็นเรื่องเกินกำลังสำหรับนักพัฒนาเดี่ยว
  • แม้แต่บริการขนาดใหญ่อย่าง Cloudflare ก็ยังใช้ tokio และ dependency จาก crates.io ตามเดิม โดยไม่ชัดเจนว่ามีการ audit บ่อยแค่ไหน
  • Clickhouse เองก็เคยกล่าวถึงปัญหาเรื่องขนาดไบนารีและจำนวน crate
  • Cargo เองก็มีข้อจำกัดตรงที่ ระบุจำนวนบรรทัดโค้ดที่ถูกรวมเข้าในไบนารีสุดท้ายได้ไม่แม่นยำ และยังนับรวมโค้ดที่ไม่จำเป็นในแต่ละแพลตฟอร์มด้วย
  • ท้ายที่สุดแล้ว คงทำได้เพียงโยนคำถามนี้ให้ทั้งชุมชนช่วยกันหาคำตอบ

3 ความคิดเห็น

 
codemasterkimc 2025-05-11

พอลองรัน Trivy ดู ก็พบว่าเมื่อเทียบกับ js NPM หรือ java Maven แล้ว จำนวนช่องโหว่ระดับ high หรือ critical มีน้อยกว่ามากและปลอดภัยกว่า แล้วบทความนี้ต้องการจะสื่ออะไรเกี่ยวกับ Rust กันแน่?

 
GN⁺ 2025-05-10
ความเห็นบน Hacker News
  • ผมคิดว่าระบบที่เพิ่ม dependency ได้อย่าง "ง่ายดาย" โดยแทบไม่มีค่าปรับด้านขนาดหรือต้นทุน สุดท้ายก็มักจะนำไปสู่ปัญหา dependency อยู่ดี มองจากวิธีแจกจ่ายซอฟต์แวร์ตลอด 40 ปีที่ผ่านมา ยุค 80 ไลบรารีต้องซื้อด้วยเงิน และในสภาพแวดล้อมที่มีข้อจำกัดด้านพื้นที่ก็ต้องคัดมาใส่เฉพาะส่วนที่จำเป็น ทุกวันนี้กลับซ้อนไลบรารีทับบนไลบรารีไปเรื่อย ๆ แค่ import foolib บรรทัดเดียวก็ใช้ได้แล้ว และแทบไม่มีใครสนใจว่าข้างในมีอะไรบ้าง แต่ละชั้นอาจใช้ความสามารถจริงแค่ราว 5% ทว่าพอต้นไม้ลึกขึ้นก็สะสมโค้ดที่ไม่เกิดประโยชน์ สุดท้ายไบนารีง่าย ๆ ตัวเดียวก็กลายเป็น 500MiB และถึงขั้นดึง dependency มาเพียงเพื่อจัดรูปแบบตัวเลขครั้งเดียว Go กับ Rust ก็ยิ่งสนับสนุนการรวมทุกอย่างไว้ในไฟล์เดียว ทำให้ถ้าอยากใช้แค่บางส่วนก็ลำบาก ในระยะยาวทางแก้ที่แท้จริงน่าจะเป็นการติดตาม symbol/dependency แบบละเอียดสุด ๆ ให้ทุกฟังก์ชัน/ชนิดข้อมูลระบุเฉพาะสิ่งที่ต้องใช้จริง แล้วดึงมาเท่าที่จำเป็น ที่เหลือก็ทิ้งไป ส่วนตัวผมไม่ค่อยชอบแนวคิดนี้นัก แต่ก็ยังนึกวิธีอื่นที่จะหยุดระบบปัจจุบันที่กวาดทั้งจักรวาลมาจากต้นไม้ dependency ไม่ออก
    • คุณอาจจะยังเป็นนักศึกษาเลยยังไม่ค่อยรู้ แต่ Rust compiler ตรวจจับโค้ด ตัวแปร หรือฟังก์ชันที่ไม่ได้ใช้งานอยู่แล้ว และ IDE ส่วนใหญ่ก็ทำได้ในหลายภาษา ถ้าอย่างนั้นก็แค่ลบส่วนนั้นออกไม่ใช่หรือ? โค้ดที่ไม่ได้ใช้จะไม่ถูกคอมไพล์อยู่แล้ว
    • ตอนที่ผมทำงานกับไลบรารี Rust ที่มีต้นไม้ dependency ค่อนข้างหนัก (Xilem) แล้วลอง trim ด้วย feature flag พบว่า dependency เกือบทั้งหมดเป็นของที่จำเป็นต้องคงไว้ตามความสามารถที่ต้องใช้จริง (vulkan, PNG decoding, unicode shaping ฯลฯ) ส่วน dependency ที่ไม่จำเป็นมักเป็นตัวเล็กมาก ๆ และผมตัด serde_json ออกได้ด้วยการแก้เล็กน้อยเท่านั้น ส่วน dependency ใหญ่กว่า (เช่น winit/wgpu) ต้องเปลี่ยนโครงสร้างถึงจะถอดออกได้ จึงไม่ได้เอาออกกันง่าย ๆ
    • Go กับ C#(.NET) เป็นตัวอย่างโต้แย้งที่ดี ทั้งสองมี package management และ ecosystem ที่มีประสิทธิภาพพอ ๆ กับ Rust หรือ JS(Node) แต่กลับมีปัญหา dependency hell น้อยกว่าเมื่อเทียบกัน เหตุผลคือ standard library แข็งแรงมาก ความใหญ่โตของ standard library แบบนี้เป็นสิ่งที่มีแต่บริษัทยักษ์ใหญ่ (Google, Microsoft) เท่านั้นที่ลงทุนได้
    • ถ้าอย่างนั้นแล้วทำไม compiler ปัจจุบันถึงไม่ลบโค้ดที่ไม่ได้ใช้งานออก?
    • เมื่อก่อนจะสร้างไฟล์ .o แยกต่อฟังก์ชันแล้วรวมเป็น archive .a จากนั้น linker จะดึงเฉพาะฟังก์ชันที่ต้องใช้มาใช้จริง การทำ namespace ก็เป็นแบบ foolib_do_thing() ปัจจุบันกลับกลายเป็นแนว god object ที่มีทุกฟังก์ชันอยู่ใน top-level object เดียว พอ import foolib ทีเดียวทั้งก้อนก็ถูกดึงมาหมด แบบนี้ linker ก็ยากจะตัดสินได้ว่าฟังก์ชันไหนจำเป็นจริง ๆ แทนที่จะเป็นเช่นนั้น Go กลับทำ dead code elimination ได้ดีมาก ถ้าไม่ใช้ก็จะถูกตัดออกจากผลลัพธ์คอมไพล์
    • compiler และ linker สมัยใหม่ก็ทำ symbol extraction และ dead code elimination อยู่แล้ว และ Rust ก็รองรับเรื่องนี้ผ่านโปรเจกต์อย่าง min-sized-rust
    • เมื่อก่อนเรามักจัดการทุกไลบรารีด้วยการรวมเข้าโปรเจกต์และบูรณาการเข้ากับไฟล์ build เองโดยตรง มันใช้ความพยายามมากและน่ารำคาญ แต่ก็ทำให้เราเข้าใจมันลึกกว่าการเพิ่มแค่บรรทัดเดียวในไฟล์ deps มาก
    • จริง ๆ แล้ว Go ไม่ได้ยึดติดกับไฟล์เดียว แต่รองรับการแยกไฟล์เชิงตรรกะได้ง่ายมาก ซึ่งเป็นจุดที่ผมชอบมาก
    • Dotnet ได้ทำแนวคิดนี้ให้เป็นจริงแล้วผ่าน Trimming และ Ahead Of Time Compilation ภาษาอื่น ๆ น่าจะเรียนรู้จาก Dotnet ได้
    • ในมุมมองเรื่องขนาดไบนารี ปัญหานี้ถูกแก้จนหมดด้วย LTO(Link Time Optimization) แล้ว ส่วนที่ไม่ใช้จะถูกตัดออกด้วย optimization แต่เวลา build ก็ยังคงใช้เวลาอยู่ดี
    • ผมกลับมองว่าปัญหาไม่ได้อยู่ที่ตัวไลบรารีเอง แต่อยู่ที่หลังจากเพิ่ม dependency ไปแล้ว เรามองไม่เห็นว่าข้างในมีอะไรถูกใช้มากน้อยแค่ไหนมากกว่า เราต้องการสภาพแวดล้อมที่ให้ feedback ได้ง่ายเกี่ยวกับประสิทธิภาพ/สัดส่วนโค้ดส่วนเกินตอน build แยกตามแต่ละแพ็กเกจ
    • ภาษาที่ชื่อ Unison ใช้วิธีที่คล้ายแนวคิดนี้บางส่วน แต่ละฟังก์ชันถูกนิยามตามโครงสร้าง AST แล้วถูกดึงจาก global registry แบบ hash-based เพื่อใช้ซ้ำ
    • แทนที่จะใช้ไลบรารีชิ้นเล็กจำนวนมากแบบ npm อย่าง isEven, isOdd, leftpad ซึ่งดูแลรักษาแบบกระจัดกระจาย ไลบรารีอเนกประสงค์ขนาดใหญ่ที่ทีมรวมศูนย์ดูแลกลับให้ความต่อเนื่องและความมั่นคงในอนาคตได้มากกว่า
    • แทนที่จะไล่ตาม ultra-fine-grained symbol/dependency อาจใช้แนวคิดโมดูลที่ละเอียดมาก ๆ ควบคู่กับระบบ tree-shaking ที่มีอยู่แล้วก็ได้
    • วิธีจัดการ dependency จริงของ Go ค่อนข้างใกล้กับอุดมคติที่บทความต้นฉบับอธิบาย โมดูลคือชุดของแพ็กเกจ และเวลาทำ vendoring ก็จะรวมเฉพาะแพ็กเกจและ symbol ที่ใช้จริงเท่านั้น (แม้ผมจะไม่แน่ใจว่ามันทำงานในระดับ symbol อย่างเคร่งครัดหรือไม่)
    • ระบบโมดูลของ JS รองรับการจัดการ symbol แบบละเอียดสุด ๆ และ tree shaking อยู่แล้ว
    • แนวคิด dependency แบบละเอียดสุดที่เสนอไว้เดิมนั้น ใน Rust ถูกแก้ไปแล้วผ่าน section splitting อย่าง --gc-sections
    • Rust เป็นภาษาที่ทำ micro import ได้ดีมากผ่านการแบ่ง API ด้วย crate feature ซึ่งต่างจาก Go
    • ขึ้นอยู่กับสถาปัตยกรรม ถ้าเป็นแบบ local-first thick client ต่อให้ติดตั้งครั้งแรก 800MB ก็อาจไม่ใช่ปัญหา เพราะตอนใช้งานจริงต้องสื่อสารผ่านเครือข่ายน้อยมาก จึงยอมรับ dependency ขนาดใหญ่ซ้ำ ๆ เพื่อการทำงานร่วมกันใน UI ได้
    • วิธีที่ดีที่สุดสำหรับการนำโค้ดกลับมาใช้ซ้ำก็คือการใช้ dependency แบบนี้แหละ ควร optimize เฉพาะจุดที่จำเป็นจริง ๆ
    • ตั้งแต่ยุค 80 แนวคิดซอฟต์แวร์คอมโพเนนต์ที่นำกลับมาใช้ซ้ำได้ก็เริ่มเป็นจริงแล้วผ่านภาษาอย่าง Objective-C หนึ่งในความสำเร็จใหญ่ของ Rust คือการทำให้แนวคิดซอฟต์แวร์คอมโพเนนต์นี้ถูกยอมรับอย่างกว้างขวางในภาษาระดับ system programming ด้วย
    • ปัญหาเรื่องขนาด/โค้ดพองสามารถบรรเทาได้ระดับหนึ่งด้วย tree shaking (และฝั่งเซิร์ฟเวอร์แทบไม่สนใจเลย) แต่ปัญหาที่หนักกว่าคือความเสี่ยงด้าน supply chain และความปลอดภัย ยิ่งเป็นองค์กรใหญ่ยิ่งมีกระบวนการอนุมัติการใช้โอเพนซอร์ส ต่อให้เพิ่ม granularity อย่างเดียวก็ไม่ช่วยด้านความปลอดภัย ถ้า 1000 ฟีเจอร์มาจากผู้เขียน NPM 1000 คน
    • ถ้าแต่ละชั้นของการ abstraction ระดับแพ็กเกจถูกใช้จริงเพียง 50% ขนาดรวมก็จะเพิ่มเป็น 2 เท่าของที่ต้องการในแต่ละชั้น แค่ 3 ชั้นก็กลายเป็นโค้ดไร้ประโยชน์ 88% แล้ว ตัวอย่างเช่นเครื่องคิดเลขใน Windows 11 ที่พ่วงไลบรารีไม่จำเป็นมาแม้กระทั่งเครื่องมือกู้คืนบัญชี นี่คือตัวอย่างที่ความง่ายในการเพิ่มฟีเจอร์นำไปสู่ความซับซ้อนที่มากขึ้น
    • ผมเห็นด้วยว่าการสะสม dependency เป็นปัญหา และแนวป้องกันที่ดีที่สุดตอนนี้คือการบริหาร dependency ระดับระบบอย่างเข้มงวดสุด ๆ แทนที่จะดึงไลบรารีภายนอกมาเพียงเพราะฟังก์ชัน 10 บรรทัดเดียว ก็อาจคัดลอกโค้ดมาใส่เองเลย ecosystem ไลบรารีที่ดีต่อสุขภาพเป็นเรื่องค่อนข้างหายาก และผมมักเบรกทันทีเมื่อวิศวกรระดับ junior เพิ่ม dependency แบบไม่ยั้งคิด
    • ไม่บ่อยนักที่จะเห็นคนพูดอย่างมั่นใจทั้งที่แทบไม่รู้อะไรพื้นฐานเกี่ยวกับ Rust เลย
    • ด้วย dead code elimination ต้นไม้ dependency ที่ใหญ่ในภาษาคอมไพล์อย่าง Rust จึงไม่ได้ทำให้ไบนารีบวมตามไปด้วย
  • ปัญหาที่ผมรู้สึกใน ecosystem ของ npm คือหลายคนดึง dependency มาใช้โดยไม่คิดเรื่องการออกแบบ ตัวอย่างเช่นไลบรารี glob ควรเป็นแค่ฟังก์ชัน globbing ธรรมดา แต่ผู้เขียนกลับยัดเครื่องมือ command line เข้ามาด้วยและเพิ่ม parser ขนาดใหญ่เป็น dependency ผลคือมีคำเตือน "dependency out-of-date" โผล่มาบ่อย อีกทั้งขอบเขตความรับผิดชอบของไลบรารี glob เองก็เป็นประเด็นถกเถียงด้วย แค่ทำ string pattern matching อย่างเดียวจะเป็นการออกแบบที่ยืดหยุ่นกว่า (ทดสอบง่ายกว่า แยก file system abstraction ได้ง่ายกว่า) แน่นอนว่ามีผู้ใช้จำนวนมากที่อยากได้ไลบรารีแบบ "ทำทุกอย่าง" แต่ยิ่งเป็นแบบนั้นผลข้างเคียงก็ยิ่งมาก Rust ก็คงไม่ได้ต่างกันมากนัก
    • เซนส์ด้านการออกแบบสำคัญมาก และภาษาที่ดีจะไม่คอยส่งเสริมหรือขัดขวางรสนิยมแบบนี้ของนักพัฒนา Rust, Zig, C เป็นตัวอย่างที่ดี ปัญหาแบบนี้เกิดน้อยกว่าในเชิงสถิติ แต่เมื่อมี "ฝูงชน" ของนักพัฒนามารวมกัน ก็จะเกิด "โมเดลตลาดนัด(bazaar)" ที่ใคร ๆ ก็สะสม crate ได้อย่างอิสระ ท้ายที่สุดผมก็หวังว่า Rust จะมี standard library อย่างเป็นทางการ (เช่น stdlib::data_structures::automata::weighted_finite_state_transducer) พร้อม namespace ที่เป็นระเบียบแบบ "batteries included" เพราะตัวภาษาเองก็มีระบบ versioning และ backward compatibility ในตัวอยู่แล้ว จึงคาดหวังการปรับปรุงต่อไปได้
    • ฟังก์ชัน glob ของ POSIX นั้นจริง ๆ แล้วสำรวจ file system ส่วน fnmatch มีไว้สำหรับ string matching ถ้าจะแยกให้ดีควรให้ fnmatch อยู่เป็นโมดูลแยกต่างหากและเป็น dependency ของ glob การเขียน glob เองขึ้นมาโดยตรงไม่ใช่เรื่องง่ายเลย เพราะมีข้อกำหนดซับซ้อนอย่างโครงสร้างไดเรกทอรี, brace expansion ฯลฯ จึงต้องอาศัยการผสมฟังก์ชันที่ออกแบบมาดี
    • ใน Rust นั้น borrow checker ทำหน้าที่เป็นเหมือนเกราะป้องกันสำหรับนักพัฒนาที่เซนส์ด้านการออกแบบยังไม่ดีนักได้อยู่ระดับหนึ่ง แต่ผลกระทบนี้จะยืนระยะได้นานแค่ไหนก็ยังไม่แน่
    • จุดแข็งใหญ่อีกอย่างของ Rust คือโดยรวมแล้วนักพัฒนามักมีฝีมือดี และคุณภาพของ crate ก็ค่อนข้างสูง
    • Bun ก็มีฟังก์ชัน glob มาให้ด้วย
  • ไม่จำเป็นต้องเจาะจงว่าเป็น Rust เลย ปัญหา dependency และการโจมตี supply chain เป็นเรื่องจริงอยู่แล้ว ถ้าจะออกแบบภาษาใหม่ ควรฝัง 'capability system' เข้ามาเพื่อแยกทั้งต้นไม้ไลบรารีให้อยู่ในขอบเขตปลอดภัยได้ เช่นเวลาออกแบบไลบรารีโหลดรูปภาพ ก็ให้มันรับแค่ stream แทนไฟล์ หรือระบุชัด ๆ ว่า "ไม่มีสิทธิ์เปิดไฟล์" แล้วบล็อกการใช้ฟังก์ชันอันตรายในระดับ compile time ไปเลย แนวคิดนี้คงทำได้ยากใน ecosystem ปัจจุบัน แต่ถ้าทำจริงก็จะลด attack surface ได้มาก วัฒนธรรมการลด dependency อย่างเดียวคงแก้ปัญหาที่รากไม่ได้ และภาษาพวก Go เองก็ไม่ได้ปลอดจาก supply chain attack เช่นกัน
    • เราควรผลักดันวัฒนธรรม Sans-IO (การออกแบบที่ไม่ให้ dependency ทำ IO โดยตรง) อย่างจริงจัง เวลามีไลบรารีใหม่ออกมา ก็ควรมีวัฒนธรรมช่วยกันทักท้วงถ้ามันทำ IO เองโดยตรง แน่นอนว่าการพิจารณาจากมวลชนอย่างเดียวไม่พอ แต่ถ้าหลักการ Sans-IO แพร่หลายขึ้นก็คงดี
    • มีตัวอย่างเป็นภาษาเฉพาะทางชื่อ WUFFS ซึ่งในทางปฏิบัติแม้แต่ Hello world ก็ยังพิมพ์ไม่ได้ และไม่มี string type ด้วย แต่ถูกออกแบบมาเฉพาะสำหรับ parse ไฟล์ฟอร์แมตที่ไม่น่าเชื่อถือเท่านั้น เราน่าจะมีภาษาเฉพาะทางแบบนี้เพิ่มขึ้นอีก มันเร็วและไม่มีความเสี่ยง จึงลดการตรวจเช็กที่ไม่จำเป็นลงได้
    • Java กับ .NET Framework เคยมีฟีเจอร์ partial trust/capabilities มาหลายสิบปีก่อน แต่ไม่ได้ถูกใช้แพร่หลายและสุดท้ายก็เลิกไป
    • Rust เองก็มีแนวโน้มคล้ายกันเล็กน้อย ผ่าน #![deny(unsafe_code)] ที่จะทำให้เกิด compile error เมื่อมีการใช้ unsafe code และแจ้งผู้ใช้ให้รู้ เพียงแต่มันไม่ใช่การบังคับตรวจสอบทุกจุด และถ้าอนุญาตเป็นพิเศษก็ยังใช้ unsafe code ได้ เราอาจจินตนาการถึง capability system ที่ควบคุมความสามารถของ standard library แบบส่งต่อกันได้คล้าย feature flag
    • ผมอยากสร้างอะไรแบบนี้ขึ้นมาจริง ๆ และหวังว่าสักวันจะเกิดขึ้นได้ ใน Rust มีความเป็นไปได้บางส่วนที่จะติดตาม capability ผ่าน linter ได้ แต่ปัญหา unsoundness ของ compiler ยังต้องแก้
    • แม้จะยากมากที่จะใส่การบังคับแบบ static อย่างสมบูรณ์ลงในภาษาและ ecosystem ที่มีอยู่ แต่แค่ตรวจสอบตอน runtime ก็ให้ผลได้เกือบทั้งหมด หากคอมไพล์โค้ดไลบรารีจาก source เราสามารถใส่ wrapper ตรวจสิทธิ์ทุก system call ได้ และถ้าฝ่าฝืนก็ให้ panic พร้อมทั้งเขียน/แจกจ่าย capability profile แยกตามแต่ละไลบรารี นี่เป็นสิ่งที่พิสูจน์แนวทางคล้ายกันมาแล้วใน TypeScript
    • Haskell ทำแนวคิดนี้ได้ระดับหนึ่งผ่าน IO monad โดยฟังก์ชันที่ทำ IO โดยตรงไม่ได้จะถูกจำกัดผ่าน type signature
    • เท่าที่ผมคิด เราอาจต้องเปลี่ยนวิธีสื่อสารกับ OS สำหรับระบบแบบนี้ไปเลย เพราะแม้แต่การอ่าน stream ก็ยังอาจใช้ system call สำหรับอ่านไฟล์จริงอยู่ดี ซึ่งเป็นกับดักหนึ่ง
    • มีโปรเจกต์ชื่อ Capslock ที่ทำงานในแนวทางใกล้เคียงนี้บน Go
    • ถ้าเริ่มจำกัดตั้งแต่โปรแกรม entry ไม่ให้ไลบรารี import system API ได้ ก็อาจส่งต่อ capability ผ่าน dependency injection ได้ในภาษาที่มีอยู่ตอนนี้ ปัญหาในทางปฏิบัติคือมันทำให้เข้ากันไม่ได้กับไลบรารีเดิม
    • ผมสงสัยว่ามีใครเคยทำอะไรคล้าย ๆ แนวคิดนี้มาก่อนหรือไม่ เพราะรู้สึกว่านำไปใช้กับภาษาปัจจุบันได้ยากมาก
    • ทำด้วยภาษาเดียวไม่น่าพอ ต้องเป็น ecosystem หลายภาษาร่วมกัน
    • ใน ecosystem ของ TypeScript เช่น ถ้าอยู่ในสภาพแวดล้อมที่ไม่มีคลาสจัดการไฟล์ การคอมไพล์ก็จะล้มเหลว ทำให้ข้อจำกัดถูกบังคับใช้อย่างเป็นธรรมชาติ
  • นี่เป็นปัญหาทั่วไปของการพัฒนาซอฟต์แวร์สมัยใหม่ กำแพงการเริ่มต้นต่ำลง และมีการนำโค้ดเดิมกลับมาใช้มากขึ้น dependency เองก็คือโค้ดที่เชื่อถือไม่ได้ในท้ายที่สุด ถ้าไม่มีทางแก้เชิงเทคนิค ก็ต้องมีคนคอยตรวจโค้ด บำรุงรักษา และรักษาระบบความเชื่อถือทางสังคม/กฎหมายต่อไปเรื่อย ๆ หากดึงเข้ามาไว้ใน Rust stdlib ทีมแกนกลางก็ต้องรับผิดชอบโค้ดทั้งหมดนั้น ภาระในการดูแลจึงสูงขึ้น
    • ระดับความรุนแรงที่มองเห็นภายนอกต่างกันไปตามภาษา ภาษาที่มี standard library แข็งแรงทำอะไรได้เยอะโดยแทบไม่ต้องพึ่ง dependency ภายนอกย่อมได้เปรียบ ส่วนภาษาอย่าง JS/Node ที่มีฟังก์ชันพื้นฐานน้อย dependency ภายนอกจึงแทบเป็นค่าเริ่มต้น ความ "เบา" ไม่ได้ดีเสมอไป
    • ผมคิดว่า Rust ควรรวมสิ่งต่าง ๆ เข้า standard library มากกว่านี้ Go มี standard library ที่ยอดเยี่ยม แต่ Rust แม้แต่ความสามารถพื้นฐานอย่าง web, tls, x509, base64 ฯลฯ ก็ยังปวดหัวกับการเลือกและดูแลไลบรารี
    • Gilad Bracha เคยเสนอแนวทางที่น่าสนใจสำหรับ sandbox ไลบรารี third-party: ตัด import ออกแล้วทำทุกอย่างผ่าน dependency injection ถ้าไม่ inject ของอย่าง IO subsystem เข้าไป โค้ด third-party ก็จะเข้าถึงมันไม่ได้เลย หากอยากให้สิทธิ์แค่อ่าน ก็แค่ wrap ความสามารถอ่านแล้ว inject ให้เท่านั้น แต่ในงาน system programming ก็ยังมีข้อจำกัด (เช่นเพราะ unsafe code)
    • มีคนเสนอด้วยว่าอาจทำแบบ QubesOS คือรันทุกไลบรารีในสภาพแวดล้อมแยกกัน โค้ดของตัวเองอยู่ใน dom0, แต่ละไลบรารีอยู่ใน template VM แยก และสื่อสารผ่าน network namespace วิธีนี้ใช้งานได้จริงในอุตสาหกรรมที่อ่อนไหว
    • เท่าที่ผมเห็น ไม่ใช่ว่าเรากำลังทำงานที่ซับซ้อนขึ้น แต่เรากำลังทำงานเดิมด้วยวิธีที่ซับซ้อนขึ้นมากกว่า เป้าหมายไม่ได้ยากขึ้นเอง
    • จริง ๆ แล้วแต่ละภาษาสถานการณ์ต่างกัน C/C++ เพิ่ม dependency ได้ยาก และถ้าจะรองรับข้ามแพลตฟอร์มก็ยิ่งยุ่ง จึงไม่ค่อยเกิดปัญหาแบบเดียวกัน
    • สิ่งที่ซับซ้อนคือโค้ดบวมที่ไม่จำเป็น เกือบทุกโปรเจกต์เต็มไปด้วยความซับซ้อนและการออกแบบเกินความจำเป็น นี่คือปัญหาของทั้งอุตสาหกรรม
  • blessed.rs แนะนำรายชื่อไลบรารีที่มีประโยชน์แต่เอาใส่ standard library ได้ยาก ผมชอบระบบนี้เพราะช่วยให้แพ็กเกจส่วนใหญ่ถูกจำกัดขอบเขตตามวัตถุประสงค์เฉพาะและบริหารจัดการได้
    • cargo-vet ก็เป็นตัวที่แนะนำได้ มันช่วยติดตามและกำหนดแพ็กเกจที่เชื่อถือได้ เช่นกำหนดให้บางแพ็กเกจต้องผ่านการ audit จากผู้เชี่ยวชาญก่อนนำเข้า หรือใช้นโยบายกึ่ง YOLO อย่างเชื่อแพ็กเกจที่ดูแลโดย maintainer ของ tokio ไปเลยก็ได้ มันเป็นทางการกว่า blessed.rs เล็กน้อย และเหมาะจะใช้แชร์รายชื่อกึ่งมาตรฐานอย่างเป็นทางการภายในทีม
    • ถ้ามีระบบแบบนี้ใน Python ก็คงดีมาก
    • พอไปดูแล้วเป็นโปรเจกต์แนะนำที่ดีจริง ๆ
  • หลังเหตุการณ์ leftpad หลายคนยังมีมุมมองลบกับ package manager อยู่ แต่ของอย่าง tokio นั้นแทบเป็นฟีเจอร์ระดับภาษาอยู่แล้ว ถ้า OP มองว่าต้อง audit เองแม้แต่ทั้ง Go หรือ V8 ของ Node ด้วย ก็คงไม่สมจริง
    • ในความเป็นจริง tokio เองก็มีคน audit อยู่เรื่อย ๆ ไม่ได้มีคนจำนวนมาก แต่ก็มีคนทำอยู่
    • cargo รองรับพฤติกรรมที่เมื่อ dependency สองตัวใช้คนละเวอร์ชัน ก็รวมทั้งสองเวอร์ชันเข้าไปได้ ซึ่งถือว่าเป็นความสามารถเฉพาะตัวของ cargo
  • feature flag ในแพ็กเกจ cargo เป็นข้อดีที่ยอดเยี่ยมจริง ๆ ผมมักส่ง PR เพื่อซ่อน dependency ที่ไม่จำเป็นไว้หลัง flag พวกนี้บ่อย ๆ และ cargo tree ก็ช่วยให้ดูต้นไม้ dependency ได้ง่าย มุมมองเรื่องจำนวนบรรทัดโค้ดที่เข้าไปอยู่ในไบนารีจริง ๆ ไม่ค่อยมีความหมายเท่าไร เพราะเวลาฟังก์ชันถูก inline มันมักจะถูกรวมเข้า main ไปเสียส่วนใหญ่
    • เสียดายที่ npm ไม่มี feature flag ไม่แน่ใจว่ามี package manager ตัวไหนรองรับอยู่แล้วหรือเปล่า ผมอยากขยายไลบรารีภายในโดยแยกโค้ดที่ขึ้นกับเฟรมเวิร์กบางตัวออกไปให้ชัด
  • ผมก็รู้สึกคล้ายกัน การเพิ่ม dependency ด้วย Cargo มันง่ายเกินไป ถึงเราจะระวังแค่ไหน แต่เพิ่มไม่กี่ตัวก็มี dependency ทางอ้อมตามมาเป็นสิบ ๆ ตัวแล้ว แน่นอนว่าจะบอกว่าไม่ต้องใช้เลยก็คงไม่สมจริง ใน C++ ปรากฏการณ์แบบนี้เกิดน้อยกว่า Rust มีการแยกเป็นแพ็กเกจเล็ก ๆ เยอะ จนให้ความรู้สึกเหมือนดึงโค้ดสุ่มจากอินเทอร์เน็ตมาใช้ ผมชอบ Rust เองนะ แต่ไม่ชอบโครงสร้างแบบนี้
    • ในบทความที่ถูกลิงก์จากซับเรดดิต Rust มีคนอธิบายว่าเหตุผลที่ C++ ดูเหมือน dependency น้อยกว่า เป็นเพราะส่วนใหญ่แจกจ่ายเป็น dynamic library อยู่แล้ว อีกมุมหนึ่งนี่ก็เป็นข้อดีเพราะพึ่งความสามารถของตัวจัดการแพ็กเกจระดับ OS ในการดูแลเรื่องเสถียรภาพ/ความปลอดภัย ผมคิดว่า Rust ควรมีแนวคิดแบบ standard library ภาคขยายสักอย่าง
    • dependency ของ C++ ก็ซับซ้อนและระบบ build ก็เละอยู่แล้ว ดังนั้นผมกลับคิดว่า dependency แบบ Rust ที่เสถียรน้อยกว่ายังดีกว่า dependency ทางอ้อมของ C++ จริง ๆ แค่ถูกซ่อนอยู่ในรูปแบบ precompiled มากกว่า
    • การแยกเป็นแพ็กเกจเล็ก ๆ ใน Rust ไม่ได้เป็น "ปรัชญา" เท่าไร แต่เกิดจากความเร็วในการ build มากกว่า พอโปรเจกต์ใหญ่ขึ้นก็ต้องแตกมันออกเป็น crate ย่อย ๆ ไม่ใช่เพื่อ abstraction แต่เป็นการปรับโครงสร้างที่ถูกบังคับด้วยประสิทธิภาพการ build
    • เราไม่จำเป็นต้องเห็นด้วยกับตรรกะว่า "งั้นก็อย่าใช้เองสิ" แบบอัตโนมัติ ควรคิดต่ออีกหน่อย
    • C++ กับ CMake ยากเกินไป จนในทางปฏิบัติซอฟต์แวร์จำนวนมากจึงลงเอยด้วยการไม่ถูกนำมาใช้เลย
  • ไลบรารีแกนหลักผมจะใช้โอเพนซอร์ส ส่วนความสามารถเล็ก ๆ จะอาศัยดูจากโอเพนซอร์สแล้วคัดลอกมาวางในโค้ดตัวเอง วิธีนี้ทำให้โค้ดใหญ่ขึ้นแบบไม่จำเป็นบ้าง แต่ลดภาระในการตรวจสอบโค้ดภายนอกและลดการเปิดรับความเสี่ยง supply chain ได้ ไลบรารีใหญ่ ๆ ก็ยังเป็นปัญหาอยู่ดี แต่ก็ไม่อาจเขียนทุกอย่างเองได้ นี่ไม่ใช่ปัญหาเฉพาะของ Rust แต่เป็นปัญหาทั่วไป
  • เมื่อก่อนผมเคยตั้งนโยบายให้ระบบสำคัญ (ในภาษาอื่น) ใช้โมดูล/แพ็กเกจน้อยที่สุดเท่าที่จะทำได้ และย้ายแพ็กเกจที่ใช้ทั้งหมดเข้า repository ภายในองค์กร จากนั้นแตก branch และ audit ตามแต่ละการอัปเดต แต่ในงานบางด้านอย่าง frontend การจัดการเข้มงวดแบบนี้แทบเป็นไปไม่ได้ ทุกวันนี้แม้แต่เครื่องมือ/โมเดล AI โอเพนซอร์สที่ดัง ๆ ก็เผชิญปัญหาคล้ายกันด้านการจัดการ dependency ผมทำโปรเจกต์ส่วนตัวด้วย Rust แล้วสิ่งที่รู้สึกอึดอัดที่สุดก็คือ dependency ของไลบรารี UI/async ที่พุ่งพรวด ถ้าตัวใดตัวหนึ่งมีช่องโหว่ก็เจาะเข้ามาได้ ซึ่งคงเป็นเรื่องของเวลา
    • วิธีที่พอเป็นจริงได้คือให้ระบบ CI/CD เชื่อมต่อกับ repository ภายในอย่างเป็นทางการเท่านั้น นักพัฒนาจะติดตั้งอะไรก็ได้ในเครื่องตัวเอง แต่ commit ที่ไม่ได้รับอนุญาตจะถูกบล็อกที่ build server
    • มี RFC ที่พยายามแก้ความเสี่ยงด้านความปลอดภัยนี้อยู่ แต่ด้วยเหตุผลทางวัฒนธรรม (น่าจะใช่) ก็ยังไม่มีการเปลี่ยนแปลงใหญ่แบบฉับพลัน
    • สิ่งที่เจ๋งของ Rust คือแม้แต่ async ก็ยังสามารถทำ implementation เองในแบบที่ต้องการได้ ไม่ได้ถูกผูกติดกับ implementation ใด implementation หนึ่ง
 
iolothebard 2025-05-11

ไม่ใช่ปัญหาเฉพาะของ Rust หรอก
มันเป็นทั้งข้อดีร่วมกันและปัญหาที่แฝงอยู่ของทุกภาษาที่มีตัวจัดการแพ็กเกจซึ่งรองรับคลังแพ็กเกจสาธารณะและการพึ่งพาแบบส่งต่อ
สุดท้ายก็ขึ้นอยู่กับคนที่เอาไปใช้ว่าจะใช้อย่างระมัดระวังแค่ไหน…
แม้จะมีกรณี leftpad ของ Node&npm มาก่อน ก็ยังไม่มีอะไรเปลี่ยนแปลง