modular_architecture_on_ios

Vì ở chương trước chúng ta đã đề cập đến hệ thống build của Xcode, nên sẽ không công bằng nếu bỏ qua swiftc. Mặc dù việc hiểu cách hoạt động của trình biên dịch không phải là kiến thức bắt buộc, nhưng nó lại rất thú vị và giúp bạn có được cái nhìn toàn diện về toàn bộ quá trình — từ việc viết mã dễ đọc cho con người đến khi chạy nó trên phần cứng thực tế. Trong chương này, chúng ta sẽ tìm hiểu cách một thư viện có thể được xây dựng hoàn toàn trong hệ sinh thái của Swift.

Trong khi các chương khác là cần thiết để hiểu rõ về phát triển kiến trúc mô-đun, thì chương này là tùy chọn.

Kiến trúc Trình Biên Dịch
Để hiểu đầy đủ về kiến trúc của trình biên dịch Swift và quy trình của nó, hãy cùng xem qua tài liệu do swift.org cung cấp và thực hiện một số ví dụ thực tế dựa trên đó.

Hình ảnh dưới đây mô tả kiến trúc của swiftc. Nó bao gồm bảy bước, sẽ được giải thích trong các tiểu mục sau.

Để minh họa, tôi đã chuẩn bị hai tệp mã nguồn Swift đơn giản: employee.swiftmain.swift. Tệp employee.swift là mã nguồn độc lập, trong khi main.swift yêu cầu Employee được liên kết dưới dạng thư viện. Tất cả các bước của trình biên dịch sẽ được giải thích dựa trên employee.swift, nhưng cuối cùng, mã nguồn của employee sẽ được tạo thành một thư viện mà tệp main sẽ sử dụng.

main.swift:

Phân tích cú pháp(Parsing)
Bộ phân tích cú pháp là một bộ phân tích đệ quy đơn giản (được triển khai trong lib/Parse) với bộ phân tích từ vựng thủ công tích hợp sẵn. Bộ phân tích cú pháp chịu trách nhiệm tạo ra Cây Cú Pháp Trừu Tượng (AST) mà không kèm theo thông tin ngữ nghĩa hay kiểu dữ liệu, đồng thời phát ra cảnh báo hoặc lỗi nếu có vấn đề về ngữ pháp trong mã nguồn đầu vào.

Source: swift.org

Bước đầu tiên trong quá trình biên dịch là phân tích cú pháp. Như định nghĩa đã nêu, bộ phân tích cú pháp chịu trách nhiệm kiểm tra cú pháp từ vựng mà không thực hiện kiểm tra kiểu dữ liệu. Lệnh sau sẽ in ra Cây Cú Pháp Trừu Tượng (AST) đã được phân tích.

swiftc ./employee.swift -dump-parse

Trong kết quả đầu ra, bạn có thể nhận thấy các kiểu dữ liệu chưa được phân giải và kết thúc bằng các lỗi.

Từ Cây Cú Pháp Trừu Tượng (AST) đã phân tích, ta có thể thấy nó rất mô tả chi tiết. Mã nguồn của employee.swift có 47 dòng, trong khi AST đã phân tích mà chưa kiểm tra kiểu dữ liệu lại có đến 270 dòng.

Tò mò, hãy cùng xem cây sẽ trông như thế nào khi có lỗi cú pháp. Để làm điều đó, tôi đã thêm “quán quân bất bại” trong trò trốn tìm — dấu chấm phẩy (;) — vào khai báo protocol.

Sau khi chạy lại lệnh tương tự, ta có thể thấy lỗi cú pháp ở phần khai báo biến houseNo. Đây là lỗi mà Xcode sẽ hiển thị ngay khi nó kiểm tra kiểu dữ liệu của tệp nguồn.

(var_decl range=[./employee.swift:4:9 – line:4:9] “houseNo” type=”./employee.swift:4:9: error: property in protocol must have explicit { get } or { get set } specifier var houseNo: Int; { get }

Phân tích ngữ nghĩa(Semantic analysis)
Phân tích ngữ nghĩa (được triển khai trong lib/Sema) chịu trách nhiệm nhận Cây Cú Pháp Trừu Tượng (AST) đã phân tích và chuyển đổi nó thành một dạng AST hoàn chỉnh, được kiểm tra đầy đủ về kiểu dữ liệu, đồng thời phát ra cảnh báo hoặc lỗi nếu có vấn đề ngữ nghĩa trong mã nguồn. Phân tích ngữ nghĩa bao gồm suy luận kiểu dữ liệu và, khi thành công, cho biết có thể an toàn để sinh mã từ AST đã được kiểm tra kiểu.

Source: swift.org

Sau bước phân tích cú pháp là bước phân tích ngữ nghĩa. Theo định nghĩa, ta sẽ thấy AST đã phân tích được kiểm tra đầy đủ về kiểu dữ liệu. Thực thi lệnh sau sẽ cho chúng ta kết quả đó.

swiftc ./employee.swift -dump-ast

Trong kết quả đầu ra, tất cả các kiểu dữ liệu đều được trình biên dịch phân giải và nhận diện, và các lỗi không còn xuất hiện nữa.

Không có gì ngạc nhiên khi sử dụng một kiểu dữ liệu không xác định, lệnh sẽ cho ra lỗi.

public protocol Address { var houseNo: Foo { get }

Trình nhập Clang(Clang importer)
Trình nhập Clang (được triển khai trong lib/ClangImporter) nhập các module Clang và ánh xạ các API của C hoặc Objective-C mà chúng xuất thành các API tương ứng trong Swift. Các AST được nhập này sau đó có thể được tham chiếu trong quá trình phân tích ngữ nghĩa.

Source: swift.org

Bước thứ ba trong quá trình biên dịch là trình nhập Clang. Đây là cơ chế kết nối nổi tiếng giữa các ngôn ngữ C/Objective-C với các API của Swift và ngược lại.

SIL generation
Swift Intermediate Language (SIL) là một ngôn ngữ trung gian cấp cao, đặc thù cho Swift, phù hợp cho việc phân tích và tối ưu hóa mã Swift. Giai đoạn sinh mã SIL (được triển khai trong lib/SILGen) chuyển đổi AST đã được kiểm tra kiểu thành SIL “thô” (raw SIL). Thiết kế của SIL được mô tả trong tài liệu docs/SIL.rst.

Source: swift.org

Bước thứ tư trong quá trình biên dịch là Swift Intermediate Language (SIL). Bạn có tò mò nó trông như thế nào không? Để in ra SIL, chúng ta có thể sử dụng lệnh sau.

swiftc ./employee.swift -emit-sil

Trong kết quả đầu ra, chúng ta có thể thấy các bảng witness, bảng vtables và bảng phân phối thông điệp cùng với các khai báo trung gian khác. Thật tiếc, phần giải thích chi tiết về những thứ này nằm ngoài phạm vi của cuốn sách này. Bạn có thể tìm hiểu thêm về các chủ đề này trong bài viết về phương thức phân phối (method dispatch).

Hơn nữa, SIL phải trải qua hai giai đoạn tiếp theo: biến đổi đảm bảo và tối ưu hóa.

Biến đổi đảm bảo SIL: Các biến đổi đảm bảo SIL (được triển khai trong lib/SILOptimizer/Mandatory) thực hiện thêm các chẩn đoán luồng dữ liệu ảnh hưởng đến tính đúng đắn của chương trình (chẳng hạn như việc sử dụng biến chưa được khởi tạo). Kết quả cuối cùng của các biến đổi này là SIL ở dạng “chuẩn hóa” (canonical).

Source: swift.org

Tối ưu hóa SIL: Các tối ưu hóa SIL (được triển khai trong lib/Analysis, lib/ARC, lib/LoopTransforms và lib/Transforms) thực hiện các tối ưu hóa cấp cao, đặc thù cho Swift đối với chương trình, bao gồm (ví dụ) tối ưu hóa Quản lý Tham chiếu Tự động (ARC), loại bỏ ảo hóa (devirtualization), và chuyên biệt hóa generic.

Source: swift.org

Sinh mã LLVM IR
Giai đoạn sinh mã IR (được triển khai trong lib/IRGen) chuyển đổi SIL thành LLVM IR, tại bước này LLVM có thể tiếp tục tối ưu hóa và sinh mã máy.

Source: swift.org

Bước cuối cùng trong quá trình biên dịch là tạo ra IR (Biểu diễn Trung gian) cho LLVM. Để lấy IR từ swiftc, chúng ta có thể sử dụng lệnh sau:

swiftc ./employee.swift -emit-ir | more

Ở đây chúng ta có thể thấy một đoạn mã khai báo quen thuộc của LLVM. Ở bước tiếp theo, mã này sẽ được LLVM chuyển đổi thành mã máy.

Xuất file dylib
Cuối cùng, chúng ta có thể tìm hiểu cách tạo thủ công một thư viện từ mã nguồn và liên kết nó với tệp thực thi.

Lệnh sau sẽ xuất tệp employee.swift thành Employee.dylib cùng với định nghĩa module của nó. Thay vì sử dụng tham số -emit-module, ta có thể dùng -emit-object để tạo thư viện liên kết tĩnh.

swiftc ./employee.swift -emit-library -emit-module -parse-as-library -module-name Employee

Sau khi thực thi lệnh, các tệp sau sẽ được tạo ra.

Bây giờ chúng ta có thể nhập thư viện Employee vào tệp main.swift và tiến hành biên dịch. Tuy nhiên, ở đây ta phải chỉ cho trình biên dịch và trình liên kết biết vị trí của thư viện Employee. Trong ví dụ này, tôi đã đặt thư viện vào thư mục tên là Frameworks, nằm cùng cấp với tệp main.swift.

swiftc main.swift -emit-executable -lEmployee -I ./Frameworks -L ./Frameworks

Để giải thích rõ hơn, lệnh swiftc -h mô tả các tham số (flags) đó như sau:

Hoan hô, tệp thực thi đã được tạo ra cùng với thư viện liên kết! Thật không may, nó bị crash ngay khi khởi động với lỗi sau:

Dựa trên kiến thức từ chương trước, chúng ta có thể kiểm tra vị trí mà file nhị phân mong đợi thư viện nằm bằng lệnh otool -l ./main.

File nhị phân mong đợi libEmployee.dylib nằm ở cùng đường dẫn. Điều này có thể dễ dàng sửa bằng một công cụ khác là install_name. Công cụ này thay đổi đường dẫn đến thư viện được liên kết trong file thực thi chính. Nó có thể được sử dụng như sau:

install_name_tool -change libEmployee.dylib @executable_path/Frameworks/libEmployee.dylib main

Chạy lại lệnh này sẽ in ra kết quả như mong muốn:

Kết luận
Trong chương này, chúng ta đã tìm hiểu các kiến thức cơ bản về kiến trúc trình biên dịch Swift. Hy vọng chương (tùy chọn) này đã cung cấp một cái nhìn tổng quan ở mức cao và khơi gợi thêm sự tò mò về cách hoạt động của trình biên dịch. Để nghiên cứu sâu hơn, tôi xin giới thiệu các nguồn tham khảo sau:

Swift Compiler

Understanding Swift Performance

Understanding method dispatch in Swift

Method Dispatch in Swift

Getting Started with Swift Compiler Development

executable path, load path and rpath

Code Toàn Bug

Code nhiều bug nhưng biết cách giấu!

Leave a Reply

Your email address will not be published. Required fields are marked *