10 điểm bởi GN⁺ 2025-10-16 | 2 bình luận | Chia sẻ qua WhatsApp
  • SQLite được phát triển bằng ngôn ngữ C ngay từ những ngày đầu (năm 2000) vì hiệu năng, tính tương thích, ít phụ thuộc và độ ổn định
  • C có thể dùng trên gần như mọi hệ điều hành và ngôn ngữ, đặc biệt hỗ trợ chạy nhanh với vai trò thư viện cấp thấp
  • Lý do chọn C thay vì ngôn ngữ hướng đối tượng là khả năng mở rộng, khả năng được gọi từ nhiều ngôn ngữ khác nhau, cùng với việc C++ và Java còn non nớt vào thời điểm phát triển
  • SQLite có cấu trúc một tệp duy nhất với hầu như không có phụ thuộc, chỉ dùng số ít hàm tối thiểu của thư viện chuẩn C
  • Dù có tranh luận về việc viết lại bằng "ngôn ngữ an toàn" như Rust hay Go, C vẫn chiếm ưu thế về kiểm soát chất lượng, hiệu năng và khả năng gọi như thư viện

1. Vì sao C là lựa chọn tối ưu

  • SQLite đã được duy trì bằng ngôn ngữ C từ lần phát triển đầu tiên vào ngày 29 tháng 5 năm 2000 cho đến nay
    • Hiện tại chưa có kế hoạch viết lại bằng ngôn ngữ khác
  • C có khả năng kiểm soát gần với phần cứng nhưng vẫn có tính di động rất cao, nên thường được gọi là “ngôn ngữ assembly có tính di động”
  • Các ngôn ngữ khác có thể tuyên bố là “nhanh ngang C”, nhưng không có ngôn ngữ nào tuyên bố nhanh hơn C

1.1. Hiệu năng

  • Thư viện cấp thấp như SQLite bị gọi rất thường xuyên nên cần phải hoạt động cực nhanh
  • Ngôn ngữ C phù hợp để viết mã nhanh, có tính di động cao nhưng vẫn cho phép tiếp cận sát phần cứng
  • Các ngôn ngữ hiện đại khác cũng tuyên bố là ‘nhanh như C’, nhưng trong lập trình mục đích chung thì không có ngôn ngữ nào có thể tự tin nói là nhanh hơn C
  • C cho phép kiểm soát chi tiết bộ nhớ và tài nguyên CPU, nên đôi khi đạt hiệu năng nhanh hơn hệ thống tệp 35%

1.2. Tính tương thích

  • Gần như mọi hệ thống đều có thể gọi thư viện được viết bằng C
  • Ví dụ, ngay cả trên Android (dựa trên Java), SQLite vẫn có thể được dùng thông qua adaptor
  • Nếu SQLite được viết bằng Java thì sẽ không thể dùng trên iPhone (Objective-C, Swift), khiến tính phổ dụng giảm mạnh

1.3. Mức độ phụ thuộc thấp

  • Vì được phát triển dưới dạng thư viện C, nó có rất ít phụ thuộc thời gian chạy
  • Ở cấu hình tối thiểu, chỉ dùng những hàm cơ bản nhất của thư viện chuẩn C như memcmp(), memcpy(), memmove(), memset(), strcmp(), strlen(), strncmp()
  • Ngay cả ở bản dựng đầy đủ hơn, nó cũng chỉ có một vài phụ thuộc như malloc(), free(), nhập xuất tệp
  • Các ngôn ngữ hiện đại thường đòi hỏi nhiều runtime cỡ lớnhàng nghìn giao diện

1.4. Độ ổn định

  • C là một ngôn ngữ cũ, ít thay đổi và khá nhàm chán, nhưng điều đó cũng đồng nghĩa với tính dự đoán được và độ ổn định
  • Khi xây dựng một bộ máy cơ sở dữ liệu nhỏ, nhanh và đáng tin cậy như SQLite, một ngôn ngữ có đặc tả không thay đổi thường xuyên là rất phù hợp
  • Nếu đặc tả hoặc cách triển khai ngôn ngữ thay đổi liên tục, điều đó sẽ bất lợi cho tính ổn định của SQLite

2. Vì sao không được viết bằng ngôn ngữ hướng đối tượng

  • Một số lập trình viên cho rằng nếu không hướng đối tượng thì khó triển khai một hệ thống phức tạp như SQLite, nhưng so với C, thư viện viết bằng C++ hay Java sẽ khó được gọi từ các ngôn ngữ khác hơn
  • Để hỗ trợ nhiều ngôn ngữ như Haskell, Java và các ngôn ngữ khác, việc chọn thư viện C là hợp lý
  • Hướng đối tượng không phải là ngôn ngữ mà là một mẫu thiết kế, nên không bị giới hạn vào một ngôn ngữ cụ thể
    • Ngay cả trong C cũng có thể triển khai các mẫu hướng đối tượng bằng struct và con trỏ hàm
  • Hướng đối tượng không phải lúc nào cũng là cấu trúc tối ưu; đôi khi mã thủ tục rõ ràng hơn, dễ quản lý hơn và cũng cho kết quả nhanh hơn
  • Vào giai đoạn đầu phát triển SQLite (khoảng năm 2000)
    • Java còn non nớt
    • C++ thì gặp vấn đề nghiêm trọng về tương thích giữa các trình biên dịch
      → Khi đó, C là lựa chọn thực tế và an toàn nhất
  • Ngay cả hiện nay, lợi ích của việc viết lại SQLite vẫn không nhiều

3. Vì sao không được viết bằng “ngôn ngữ an toàn”

  • Gần đây, sự quan tâm tới các ngôn ngữ lập trình an toàn như Rust và Go tăng cao, nhưng vào thời điểm SQLite mới được phát triển (trong 10 năm đầu tiên), các ngôn ngữ này còn chưa tồn tại
  • Nếu viết lại bằng Go hoặc Rust, có thể sẽ phát sinh nhiều lỗi hơn hoặc hiệu năng bị giảm
  • Các ngôn ngữ này chèn thêm mã nhánh (branch) cho việc kiểm tra bộ nhớ và các cơ chế khác; trong khi với chiến lược chất lượng của SQLite, độ phủ nhánh 100% là rất quan trọng, nhưng phần này chưa được đáp ứng
  • Các ngôn ngữ an toàn thường dừng chương trình khi xảy ra tình trạng out-of-memory, nhưng SQLite được thiết kế để có thể phục hồi ngay cả trong tình trạng thiếu bộ nhớ
  • Rust, Go và các ngôn ngữ tương tự vẫn còn là ngôn ngữ mới, cần thêm thời gian phát triển liên tục
  • Vì vậy, đội ngũ phát triển SQLite vẫn ủng hộ sự phát triển của các ngôn ngữ an toàn, nhưng trong việc triển khai SQLite, họ vẫn coi trọng độ ổn định đã được kiểm chứng của C

Dù vậy, trong tương lai vẫn có khả năng được viết lại bằng Rust. Còn khả năng viết bằng Go thì thấp vì Go không ưa assert()

  • Tuy nhiên, để có thể được viết bằng Rust thì cần có các điều kiện tiên quyết sau:
    • Rust phải trưởng thành hơn và chu kỳ thay đổi phải chậm lại, để trở thành một “ngôn ngữ cũ và nhàm chán”
    • Phải chứng minh được rằng có thể tạo ra thư viện phổ dụng có thể được gọi từ nhiều ngôn ngữ
    • Phải tạo được mã đối tượng có thể chạy cả trên thiết bị không có hệ điều hành như hệ nhúng
    • Phải có công cụ kiểm thử độ phủ nhánh 100% cho binary đã biên dịch
    • Phải phục hồi được từ lỗi OOM (thiếu bộ nhớ)
    • Rust phải thực hiện được mọi công việc mà C đang xử lý trong SQLite mà không bị suy giảm hiệu năng
  • Nếu một người yêu Rust (rustacean) cho rằng các điều kiện trên đã được đáp ứng và SQLite nên được viết lại bằng Rust, thì họ được khuyến khích liên hệ trực tiếp với các nhà phát triển SQLite để trình bày ý kiến

2 bình luận

 
GN⁺ 2025-10-16
Ý kiến trên Hacker News
  • Dù các ngôn ngữ lập trình an toàn chưa tồn tại trong 10 năm đầu, tôi vẫn nghĩ rằng nếu triển khai lại SQLite bằng Go hay Rust thì rất có thể sẽ tạo ra nhiều lỗi hơn số lỗi có thể sửa được, và thậm chí còn chậm hơn. Nếu đã có một đoạn mã gần như không lỗi được hoàn thiện sau thời gian khổng lồ cùng rất nhiều kiểm thử, thì trong bối cảnh mức độ thay đổi thấp, nó được viết bằng ngôn ngữ nào cũng không quá quan trọng. Kể cả là assembly cũng không thành vấn đề
    • Điểm “mức độ thay đổi thấp thì ít vấn đề hơn” cũng từng được Google Security Blog giải thích. Ở đó lập luận rằng phần lớn vấn đề an toàn bộ nhớ xuất hiện trong mã mới, và mã nguồn sẽ an toàn hơn theo thời gian liên kết liên quan
    • Phía Rust thì có những dự án như Turso đang hoạt động khá tích cực Turso
    • Cũng có người cho rằng không nên viết lại các tiện ích cơ bản của Linux bằng Rust. Họ cho rằng không cần phải viết lại những phần mềm đã được dùng suốt hàng chục năm và phần lớn lỗi đã bị loại bỏ
    • Tôi nghĩ Zig phù hợp để thay thế một phần mã C. Nó cũng hòa hợp tốt với Python và các binary C hiện có. Triết lý của Go thì hay, nhưng có hạn chế là khó tối ưu và cần các lập trình viên thật giỏi. Rust cũng dùng được, nhưng việc tiếp tục dùng C hiện có và dần đưa Zig vào thì dễ hơn nhiều. Chúng ta không thể loại bỏ hoàn toàn lỗi trong mã C, nhưng tôi cảm thấy việc chuyển sang Rust là rất khó về mặt thực tế
    • Đã có sẵn một triển khai sqlite được port sang Go cznic/sqlite
  • Ngoài lý do vì sao C là lựa chọn tối ưu vào thời điểm SQLite được phát triển ban đầu và những ưu điểm hiện tại, tôi không thấy có lý do đặc biệt nào để phải viết lại SQLite bằng ngôn ngữ khác. Ai cũng có thể làm một cơ sở dữ liệu SQL nhẹ, nên hoàn toàn có thể tạo một triển khai mới bằng Rust, C++, Go, Lisp hay ngôn ngữ nào mình muốn. Không cần vứt bỏ một triển khai C đang hoạt động tốt, rồi ép những người đã duy trì SQLite bằng C hơn 25 năm phải học ngôn ngữ mới và làm lại từ đầu
    • Tôi cảm thấy trong nhiều cộng đồng hâm mộ ngôn ngữ có xu hướng ép người khác dùng thứ mình thích, và việc chọn ngôn ngữ phần nào đã bị biến thành một cuộc chiến zero-sum. Khi một dự án được phát triển bằng một ngôn ngữ nhất định, chỉ riêng việc không dùng ngôn ngữ đó cũng khiến người ta đặt câu hỏi về sự cần thiết của ngôn ngữ khác. Thực ra lựa chọn đa dạng hơn nhiều, và kể cả có viết lại bằng ngôn ngữ khác thì menu cũng còn Rust, Go, D, Lisp, Julia cùng nhiều ngôn ngữ khác
    • Thực tế các nhà phát triển SQLite vẫn cởi mở với việc rewrite bằng Rust. Nếu Rust đáp ứng được các điều kiện tiên quyết cần thiết, họ có thể sẽ làm lại. Nếu là người hâm mộ Rust thì cũng có hướng dẫn là hãy liên hệ trực tiếp với nhóm phát triển SQLite
    • Đã có các dự án như rqlite và turso được triển khai bằng Rust
    • Có adapter viết bằng Go nên có thể dùng sqlite trong golang mà không cần cgo. Giờ đây sqlite không chỉ là một thư viện C, mà còn là một định dạng tệp cơ sở dữ liệu. Tôi nghĩ rồi sẽ có một triển khai pure Rust, và một ngày nào đó nó có thể trở thành triển khai chính
    • Tôi thấy khó chịu với xu hướng hiện nay là coi mọi công nghệ quá 5 năm tuổi là lỗi thời. Tôi nghĩ cần tôn trọng hơn những công nghệ đã được mài giũa trong thời gian dài
  • Các ngôn ngữ an toàn tạo thêm các nhánh kiểm tra biên khi truy cập mảng, nhưng trong mã đúng thì những nhánh đó thực tế không bao giờ chạy. Nghĩa là rất khó đạt 100% branch test, và điểm này có liên quan đến chiến lược chất lượng của SQLite. Tôi thấy thú vị khi nghe lập luận mới này
    • Nếu có thể chắc chắn nhánh mã đó sẽ không bao giờ chạy, thì chẳng phải cũng không cần kiểm thử phần đó sao? Tôi có cảm giác như đang hy sinh tính an toàn chỉ để đạt 100% độ bao phủ kiểm thử
    • Trong các ngôn ngữ an toàn, compiler tự động thêm mã phòng vệ kiểu if (i >= array_length) panic("index out of bounds"), và bản thân đoạn mã đó đã được compiler Rust kiểm thử tốt nên có lẽ không cần lo. Tôi muốn biết liệu mình có đang hiểu đúng lập luận này không
    • Với những chuyên gia như Dr Hipp và một dự án như sqlite, tôi nghĩ lập luận này cũng có phần hợp lý
    • Dùng cách như get_unchecked() của Rust thì cũng có thể truy cập không cần bounds check, nhờ đó vẫn an toàn mà lại tăng hiệu năng tài liệu get_unchecked
    • Tôi tự hỏi liệu có thể giảm vấn đề này bằng cách không bắt buộc coverage đối với các nhánh chỉ rẽ sang panic theo điều kiện hay không
  • SQLite vẫn để ngỏ khả năng một ngày nào đó được viết lại bằng Rust, còn Go thì khó khả thi hơn do các ràng buộc liên quan tới assert(). Theo tôi, để chuyển sang Rust thì cần các điều kiện như sau: Rust phải ổn định lâu hơn và ít thay đổi hơn, phù hợp để viết thư viện dùng chung, chạy được trên embedded không có OS, cần tooling cho 100% branch coverage, cần có cơ chế xử lý lỗi OOM, và phải thay được vai trò của C mà không bị giảm hiệu năng
    • Rust từ sau 1.0 đã thay đổi tương thích trong hơn 10 năm. Có sự khác biệt giữa người muốn nó hoàn toàn ngừng thay đổi và người chấp nhận việc nó vẫn tiếp tục thay đổi. Việc phát triển thư viện dùng chung đã được chứng minh, còn hỗ trợ embedded không có OS thì rõ ràng là làm được. Về branch coverage thì tôi không phải chuyên gia nên không rõ, nhưng Ferrocene và các bên khác đang làm việc về phần này. Bản thân ngôn ngữ Rust không tự phân bổ bộ nhớ, nên xử lý OOM có thể được quyết định ở mức thư viện chuẩn. Còn vấn đề hiệu năng thì còn tùy cách định nghĩa
    • Tôi tự hỏi liệu trong Go có thể dùng cú pháp if condition { panic(err) } như một dạng hàm assert hay không
  • Phần lớn các lập luận lúc đầu nghe có vẻ hợp lý, nhưng soi kỹ thì không hoàn toàn chặt chẽ. Tôi nghĩ chỉ cần giải thích rõ vì sao đã chọn C vào khoảng năm 2000, còn hiện tại thì chấp nhận một codebase đã được mài giũa là đủ. Những lập luận bổ sung đều có điểm có thể phản biện
    • Tôi muốn nghe cụ thể lập luận nào có thể phản biện
    • Những lý do được nêu có thể dùng để duy trì codebase cũ, nhưng nếu muốn thuyết phục các lập trình viên mới chọn C thay vì ngôn ngữ phức tạp khác thì cần thêm nhiều căn cứ hơn
    • (Tài liệu này được viết vào năm 2017)
    • Tôi đoán đây là một tài liệu dài và chi tiết được viết ra để đối phó với vô số câu hỏi kiểu “vì sao không viết lại bằng X” suốt thời gian dài
  • Dự án tự động chuyển SQLite sang Go đã tồn tại và được phát hành tích cực từ nhiều năm trước modernc.org/sqlite. Nó cũng vượt qua cùng bộ test. Tuy vậy bản Go chậm hơn khá nhiều, và trong nhiều tình huống thì sự tiện lợi của một bản port native cho Go còn quan trọng hơn bản thân tốc độ. Kết luận là thay vì viết lại SQLite bằng Go, Rust, Zig, Nim, Swift hay ngôn ngữ khác, cách thực tế hơn là tự động dịch từ C sang
    • Dù vượt qua bộ test công khai, tôi nghe nói SQLite còn có bộ test nội bộ khắt khe hơn rất nhiều
    • Vượt qua test suite không có nghĩa là không có lỗi, vì vẫn có thể còn sót lại edge case mới hoặc vấn đề hiệu năng
  • “Vì sao SQLite được phát triển bằng C” đã được giải thích rõ trong tài liệu chính thức, nhưng khi nghe câu hỏi “vì sao không phải Rust” thì tôi lại nghĩ ngay “vì sao nhất định phải là Rust?”
    • Có ý kiến cho rằng điều này là do tiêu đề bị áp một góc nhìn sẵn
    • Đã có các dự án rewrite bằng Rust như thế này: tursodatabase/tursobài blog cũng có bàn về câu hỏi Why
    • Cũng có thể hỏi vì sao SQLite lại được viết bằng C chứ không phải BASIC
  • Càng đọc nhiều bài về lập trình, sử dụng phần mềm và rewrite, tôi càng thấy một vấn đề là nếu rewrite chỉ nhắm tới “tương đương chức năng”, thì rất dễ bỏ sót vô số ngoại lệ và bản vá đã tích lũy theo thời gian. Cuối cùng phần mềm lại hỏng, hoặc những thứ trước đây chạy tốt thì không còn hoạt động nữa. Kiểu rewrite này cần được nhấn mạnh và cân nhắc cực kỳ cẩn thận, vì tôi nghĩ rất khó phục hồi 100%. Các thư viện quan trọng như SDL cũng vậy. Có thể sẽ lại xuất hiện các bản phát hành liên tục hỏng và người dùng than phiền. Tôi nghĩ C sẽ còn tồn tại lâu sau cả khi Rust trở thành xu hướng chính. Rewrite không nên là lựa chọn mặc định
  • Điều thú vị hơn là DuckDB lại được viết bằng C++ chứ không phải Rust. DuckDB là dự án mới xuất hiện năm 2019, nên tưởng như có thể chọn Rust, nhưng cuối cùng vẫn chọn C++. DuckDB còn mới và codebase cũng nhỏ hơn SQLite rất nhiều
    • Tôi nghe nói nhóm phát triển DuckDB tự tin với C++ và tin vào khả năng auto-vectorization của compiler nên đã chọn như vậy. Vào thời điểm đó (2019), Rust chưa có hỗ trợ SIMD cấp cao rõ ràng. Họ không muốn phải tự duy trì mã SIMD viết tay
    • Nếu mục tiêu là hiệu năng tối đa, tôi nghĩ C++ có thể tạo ra binary nhanh hơn với ít mã hơn. C++ hiện đại cũng có độ an toàn ở thời điểm biên dịch cao hơn, nên phù hợp với các đoạn mã như cơ sở dữ liệu
    • Nếu viết bằng C++ hiện đại thì tôi thấy cũng ổn
  • Trước đây cũng đã có nhiều tranh luận về việc rewrite SQLite năm 2021, năm 2018
    • Bình luận của tptacek khá thú vị: trong tài liệu cũ có một đoạn về bảo mật nhưng trong tài liệu mới thì đã biến mất. C rõ ràng là một điểm yếu bảo mật ngay cả với SQLite. Ở phiên bản cũ có giải thích rằng ‘SQLite không phải là thư viện quá nhạy cảm về bảo mật’. Lập luận là việc thực thi SQL không đáng tin cậy tự thân đã là vấn đề lớn hơn, còn với việc nhập tệp từ bên ngoài thì dùng mã phòng vệ và kiểm thử rất mạnh để ngăn lỗi, đồng thời cũng có các quy trình xác thực trước tài liệu web archive năm 2021
 
aer0700 2025-10-16

C cũng bị xem là rủi ro bảo mật với SQLite; vậy ngay cả khi đã viết test thật kỹ và là một lập trình viên đủ dày dạn kinh nghiệm thì vẫn vậy sao? Vấn đề có thể nằm ở logic và quy trình phát triển, nhưng việc bản thân ngôn ngữ lại là lỗ hổng bảo mật thì tôi thấy khá khó hiểu. Thực ra hầu như không có chương trình nào không phụ thuộc vào hạ tầng được viết bằng C cả.