Kiểm tra khả năng tương thích Swift C với Raylib (+WASM)
Testing the Swift C compatibility with Raylib (+WASM)
Bài viết này giới thiệu cách Swift có thể tương tác mạnh mẽ với mã C mà không cần viết thêm FFI bindings thủ công. Tác giả đã xây dựng một game đơn giản sử dụng Raylib, chạy được cả trên macOS và WebAssembly (WASI). Điểm nhấn ở đây là khả năng tự động của Swift trong việc xử lý C headers và libraries nhờ Clang importer, giúp việc tích hợp trở nên liền mạch. Các bạn developer nên biết rằng Swift cung cấp một cách tiếp cận mạnh mẽ và dễ dàng để tận dụng các codebase C/C++ có sẵn, kể cả cho các ứng dụng đa nền tảng, điều này có thể thay đổi nhận định phổ biến về hệ sinh thái của Swift.
Kể từ khi nhóm Ladybird từ bỏ việc áp dụng Swift cho trình duyệt, tôi đã nghe rất nhiều lời chỉ trích về hệ sinh thái Swift và sự tương tác giữa các dự án Swift và C/C++. Việc sử dụng Swift của tôi chủ yếu dành cho các công cụ dòng lệnh, lập trình giải trí (như Advent of Code 2023 và các năm trước) hoặc lập trình Metal. Trong những thử nghiệm trước đây, tôi thực sự thích Swift và thực sự thích nó hơn một số ngôn ngữ lập trình khác như Rust. Tuy nhiên, có vẻ như các lập trình viên có quan điểm sai lầm về ngôn ngữ lập trình này, đặc biệt là về khả năng truy cập của nó (không, nó không chỉ dành cho nền tảng Apple) và các thư viện C/C++ sức mạnh thực tế của nó.
Kể từ khi nhóm Ladybird từ bỏ việc áp dụng Swift cho trình duyệt, tôi đã nghe rất nhiều lời chỉ trích về hệ sinh thái Swift và sự tương tác giữa các dự án Swift và C/C++.
Việc sử dụng Swift của tôi chủ yếu dành cho các công cụ dòng lệnh, lập trình giải trí (như Advent of Code 2023 và các năm trước) hoặc Lập trình kim loại.
Trong các thử nghiệm trước đây, tôi thực sự thích Swift , và thực sự thích nó hơn một số ngôn ngữ lập trình khác như Rust. Tuy nhiên, có vẻ như các lập trình viên có quan điểm sai về ngôn ngữ lập trình này, đặc biệt là về khả năng truy cập của nó (không, nó không chỉ dành cho nền tảng của Apple) và các thư viện C/C++ sức mạnh thực sự của nó .
Hôm nay, tôi sẽ chứng minh việc tôi xây dựng một trò chơi Raylib rất cơ bản bằng Swift, không cần FFI cũng như cho macOS và web (sử dụng WASI) dễ dàng như thế nào.
Bài viết này nhằm mục đích trình diễn chứ không phải hướng dẫn.
Vì mục đích này, tôi sẽ không giải thích cách cài đặt Swift, WASM SDK cho Swift, v.v.
Tuy nhiên, nếu bạn muốn tái tạo bản trình diễn này ở nhà, bạn có thể tìm thấy dự án đã hoàn thành trên github của tôi và điều chỉnh cho phù hợp với nhu cầu của riêng bạn.
Swift <3 Raylib #
Không giống như các ngôn ngữ khác, Swift không yêu cầu bạn viết các lớp liên kết hoặc lớp bao bọc FFI thủ công để tương tác với mã C. FFI được thiết kế để hoàn toàn vô hình và tự động thông qua trình nhập Clang (sẽ nói thêm về điều đó sau).
Điều này có nghĩa là bạn có thể trực tiếp thả tiêu đề C vào dự án của mình, thư viện C tĩnh và sử dụng sức mạnh của Trình quản lý gói Swift để cho trình biên dịch biết dự án cần được biên dịch như thế nào.
Mã tôi muốn chạy rất đơn giản: khởi tạo raylib, một cửa sổ và vẽ văn bản.
Đây là mã Swift:
nhập CRaylib
hãy screenWidth: Int32 = 800
cho phép screenHeight: Int32 = 600
#if os(WASI)
InitWindow(screenWidth, screenHeight, "WASM C Raylib từ Swift!")
#else
InitWindow(screenWidth, screenHeight, "Raw C Raylib từ Swift!")
#endif
SetTargetFPS(60)
let rayWhite = Màu(r: 245, g: 245, b: 245, a: 255)
let darkGray = Màu sắc(r: 80, g: 80, b: 80, a: 255)
trong khi !WindowShouldClose() {
Bắt đầu Vẽ()
ClearBackground(rayWhite)
DrawText("Nó còn sống... SỐNG!", 300, 300, 20, darkGray)
Kết thúc Vẽ()
}
ĐóngWindow()
Cấu trúc dự án #
Thông thường, một dự án Swift được cấu thành như thế này:
Gói.swift
Nguồn/
Tên dự án/
mã.swift
Tất cả mã trong Nguồn sẽ được Trình quản lý gói Swift sử dụng để biên dịch dự án của tôi.
Trong trường hợp của tôi, tôi muốn phân biệt những gì đến từ dự án Raylib C và mã của riêng tôi.
Vì vậy, dự án Swift của tôi kết thúc như thế này:
Mọi thứ trong Sources/CRaylib đều liên quan đến chính raylib: tệp tiêu đề, thư viện tĩnh (dựa trên nền tảng, ở đây là macOS và WASM), v.v.
Và mọi thứ trong Sources/MyGame là mã của tôi, mã này sẽ sử dụng mã raylib C.
Tôi thực sự thích tải xuống và thay thế trực tiếp các tệp trong dự án của mình thay vì để tập lệnh, trình quản lý gói hoặc thậm chí người dùng cuối tự động tải chúng xuống.
Cách tiếp cận này xuất phát trực tiếp từ kinh nghiệm của tôi trong việc phát triển công cụ trò chơi: đừng bao giờ tin rằng thư viện sẽ có sẵn trực tuyến sau một phút và đừng bao giờ tin rằng phiên bản tiếp theo sẽ không làm hỏng dự án hoặc phần phụ thuộc của bạn.
Đây thực sự là một cách "chống web" để xem mọi thứ, nhưng nó thực sự đã cứu mạng (dev-) của tôi nhiều lần.
Tệp Package.swift #
Nhưng làm cách nào để xây dựng dự án đó (bây giờ hãy gọi nó là “MyGame”)?
Hãy cùng tìm hiểu tệp Package.swift ở thư mục gốc của dự án:
// swift-tools-version: 6.2
nhập Mô tả gói
cho gói = Gói(
tên: "MyGame",
mục tiêu: [
.target(
tên: "raylib",
đường dẫn: "Sources/CRaylib",
publicHeadersPath: "..",
linkerCài đặt: [
// macOS
.unsafeFlags(["-L", "Sources/CRaylib/macOS"], .when(platforms: [.macOS])),
.linkedFramework("OpenGL", .when(platforms: [.macOS])),
.linkedFramework("Ca cao", .when(nền tảng: [.macOS])),
.linkedFramework("IOKit", .when(nền tảng: [.macOS])),
.linkedFramework("CoreVideo", .when(nền tảng: [.macOS])),
// chỉ WASM
.unsafeFlags(["-L", "Sources/CRaylib/WASM"], .when(platforms: [.wasi])),
]
),
.executableTarget(
tên: "MyGame",
phần phụ thuộc: ["raylib"]
),
]
)
Phần thú vị của phần minh họa này là cách tôi xác định mục tiêu.
Mục tiêu này thực sự là mục tiêu Clan , chỉ định cách đặt tên mục tiêu, mã nguồn ở đâu, tiêu đề và thư viện nào mà trình liên kết cần tương tác cho bước liên kết.
Trong trường hợp này, tôi có một mục tiêu có tên raylib với mã nguồn trong một đường dẫn tương đối (Sources/CRaylib) và các thư viện khác nhau tùy thuộc vào hệ điều hành (macOS hoặc WASM).
Rất dễ dàng!
Hãy chạy nó #
Nếu tôi cố chạy dự án của mình thì tôi gặp sự cố:
> chạy nhanh
cảnh báo: 'craylib': bỏ qua (các) mục tiêu đã khai báo 'craylib, MyGame' trong gói hệ thống
cảnh báo: 'craylib' : các gói hệ thống không được dùng nữa; thay vào đó hãy sử dụng mục tiêu thư viện hệ thống
lỗi: không có sản phẩm thực thi nào
Hửm, chuyện gì đang xảy ra ở đây vậy?
Swift thực chất không biết tệp tiêu đề (hoặc .h) là gì. Như trong, bạn không thể viết import raylib.h trực tiếp vào tệp Swift.
Để giải quyết vấn đề này, Apple đã tích hợp Clang Importer vào trình biên dịch Swift. Khi trình biên dịch Swift biên dịch mã Swift, Swift âm thầm khởi động Clang, phân tích các tiêu đề C, dịch chúng sang định dạng mà Swift có thể hiểu được, sắp xếp mã thành mô-đun và chuyển chúng lại cho Swift thông qua.
Tuy nhiên, tôi thực sự phải giúp trình biên dịch tạo cầu nối giữa mô-đun Clang này và Swift bằng cách sử dụng tệp module.modulemap trong dự án CRaylib của tôi:
mô-đun CRaylib [hệ thống] {
tiêu đề "raylib.h"
liên kết "raylib"
xuất *
}
Ở đây, module.modulemap có thể được giải thích như sau:
mô-đun CRaylib [hệ thống]: “Tạo một mô-đun Swift hoàn toàn mới có tên CRaylib và coi nó như một thư viện hệ thống…”,header "raylib.h": “Đây là tệp chính xác mà bạn cần phân tích cú pháp để tìm các hàm và cấu trúc C…”link "raylib": “Bất cứ khi nào tệp Swift nhập mô-đun này, hãy tự động yêu cầu trình liên kết tìm kiếm thư viện được biên dịch có tên libraylib.a…”xuất *: “Hãy sử dụng mọi hàm C mà bạn tìm thấy và cung cấp công khai cho dự án Swift của tôi”
Cấu trúc dự án cuối cùng của chúng tôi như thế này:
Gói.swift
Nguồn/
CRaylib/
macOS/
libraylib.a
WASM/
libraylib.a
raylib.h
mô-đun.modulemap
Trò chơi của tôi/
chính.swift
Bây giờ, nếu tôi cố chạy nó… tôi có một cửa sổ! Vâng!
Ok, hãy tóm tắt những gì tôi đã làm trước đây:
- Tải xuống và sao chép tiêu đề raylib và thư viện tĩnh vào dự án của tôi: DỄ DÀNG,
- Viết tệp
Package.swiftđể khai báo dự án của tôi và các phần phụ thuộc của nó: DỄ DÀNG, - Viết tệp
module.modulemapđể thực hiện chuyển đổi giữa các tệp raylib C và dự án Swift của tôi: DỄ DÀNG.
DỄ DÀNG + DỄ DÀNG + DỄ DÀNG = DỄ DÀNG.
Nhanh <3 WASM #
Được rồi, bây giờ tôi đã có bản dựng gốc… tại sao không xây dựng nó cho WASM?
Cộng đồng Swift đã thực hiện rất nhiều cải tiến trong những năm qua để xây dựng các ứng dụng WASM bằng Swift.
Nếu bạn quan tâm đến nó, tôi khuyên bạn nên xem tài liệu chính thức.
Việc xây dựng dự án cho WASM phức tạp hơn một chút, có thể vì tôi thiếu một số tài liệu để xây dựng loại dự án này bằng WASM SDK cho Swift.
Như được hiển thị tại đây Tôi đã liên kết dự án WASM với đúng thư viện tĩnh, theo đúng đường dẫn.
Và tôi không có bất kỳ sửa đổi nào để thực hiện trong tệp module.modulemap cho raylib WASM. Đẹp đấy.
Tất cả những điều sau đây chỉ là cách xây dựng dự án WASM cuối cùng:
> swift build --swift-sdk swift-6.2.4-RELEASE_wasm # tôi đang sử dụng Swift 6.2.4 và Swift SDK cho WASM đã được cài đặt
Khi tôi xây dựng cho trình duyệt, tôi gặp lỗi liên kết, nhưng tệp đối tượng của tôi đã được xây dựng bằng trình biên dịch Swift WASM.
Vì stdlib của Swift được xây dựng cho máy chủ không đầu (WASI) nên nó cần có thiết bị đầu cuối.
Tôi cần viết một đoạn mã C nhỏ để chặn các yêu cầu đầu cuối đó (errno, __wasi_args_sizes_get và __wasi_args_get) để môi trường WebGL của trình duyệt (Emscripten) không bị hoảng loạn:
// wasi_stubs.c
#include
int errno = 0;
int32_t __wasi_args_sizes_get(int32_t *argc, int32_t *argv_buf_size) {
*argc = 0;
*argv_buf_size = 0;
trở lại 0;
}
int32_t __wasi_args_get(int32_t *argv, int32_t *argv_buf) { return 0; }
và cuối cùng là biên dịch dự án bằng emcc:
emcc .build/wasm32-unknown-wasip1/debug/MyGame.build/main.swift.o ./wasi_stubs.o \
Nguồn/CRaylib/WASM/libraylib.a \
-L/usr/lib/swift_static/wasi \
-lswiftCore \
-s USE_GLFW=3 \
-s ASYNCIFY \
-o index.html
Lưu ý quan trọng
Trình duyệt không thể xử lý vòng lặp while vô hạn mà không bị treo.
Để tránh phải viết lại hàm vòng lặp vô hạn trong main.swift, tôi đã sử dụng cờ Emscripten ASYNCIFY để tạm dừng mã Swift và để trình duyệt thực sự vẽ khung.
Khi tôi đã tạo xong các tệp web của mình (index.html, index.js và index.wasm), tôi có thể tạo một máy chủ web nhỏ và truy cập http://localhost:8080 để xem…
Điều khó nhất là sử dụng emcc để tạo tệp nhị phân WASM cuối cùng của chúng tôi.
Tôi thậm chí không cần sửa đổi mã nguồn hoặc dự án của chúng tôi!
Nhiệm vụ đã hoàn thành!
Kết luận #
Việc gói C cho Swift khá đơn giản và tôi đã tạo thành công một cửa sổ và vẽ một số văn bản bằng raylib. Việc sử dụng Trình quản lý gói Swift rất hữu ích và tôi không cần phải tìm hiểu quá nhiều về các vấn đề về trình biên dịch hoặc xây dựng trình bao bọc bằng tay để thực sự tương tác với mã C của raylib.
Vì vậy, nếu bạn muốn xây dựng trò chơi bằng raylib, tại sao không học hoặc sử dụng Swift để làm việc đó?
Tác giả: LucidLynx