為何有些專案要用多種程式語言?

從網站開發到作業系統核心,許多軟體都巧妙地融合了多種語言。有些情況很直觀,例如網頁前後端分離;但有些情況則更為深入,多種語言被編譯成單一的執行檔。本頁將帶您探索這背後的底層秘密。

編譯的真相:不只是個黑盒子

我們常說「編譯器將原始碼變成執行檔」,但這其實是一個精細的多步驟過程。了解這個過程,是理解多語言混合的關鍵第一步。點擊下方各階段,看看一段簡單的C語言程式碼是如何一步步轉換的。

1 前置處理
2 編譯
3 組譯
4 連結

輸出結果:

階段說明:

連結的魔法:靜態 vs. 動態

連結」是編譯流程的最後一步,它負責將所有需要的程式碼片段組合成一個完整的執行檔。主要有兩種方式,它們在效率和彈性上有著截然不同的取捨。

混合語言實戰

正因為編譯流程是模組化的,且最終都由「連結器」來組合,我們才有可能混合不同語言。只要每種語言都能被編譯成連結器看得懂的「目的檔」,它們就能合作。

範例一:C + 組合語言

這是最常見的組合。當對效能有極致要求時,開發者會用組合語言手寫最核心的計算部分,然後由C語言來呼叫它,處理其他邏輯。

// main.c extern int calculate_primes(int max); printf("Primes: %d\n", calculate_primes(1000));
+
; primes.asm global calculate_primes calculate_primes:     ; ... 高效能的組合語言實作 ...     ret
🔗 連結器 (Linker) → 單一執行檔

範例二:C + Rust

高階語言之間也可以混合。例如,我們可以利用Rust的記憶體安全特性來編寫一個函式庫,然後在現有的C專案中呼叫它,兼顧安全與效能。

// main.c extern void process_data_from_rust(); process_data_from_rust();
+
// lib.rs #[no_mangle] pub extern "C" fn process_data_from_rust() {     // ... Rust 的安全程式碼 ... }
🔗 連結器 (Linker) → 單一執行檔

溝通的規則:應用程式二進位介面 (ABI)

能連結在一起還不夠,不同語言編譯出來的機器碼還必須遵守相同的「溝通規則」,才能正確地呼叫彼此。這個規則就叫做 ABI,它定義了函式呼叫時參數如何傳遞、回傳值放哪裡等底層細節。

情境一:ABI 不匹配

語言A認為參數要放在暫存器0和1,但語言B卻期望從暫存器1和2讀取。這種誤會會導致程式執行錯誤,甚至崩潰。

語言A (呼叫方)
R0: 參數1
R1: 參數2
語言B (被呼叫方)
R1: ???
R2: ???

情境二:ABI 一致 (例如都遵守C ABI)

透過 `extern "C"` 等關鍵字,我們可以指示編譯器遵循一個共同的標準ABI。這樣,雙方就能正確無誤地溝通。

語言A (呼叫方)
R0: 參數1
R1: 參數2
語言B (被呼叫方)
R0: ???
R1: ???

那麼,為何要這麼做?

混合多種語言雖然增加了複雜度,但在特定情境下能帶來巨大好處,這是一種工程上的權衡取捨。

🚀

極致的效能

用高階語言快速開發大部分功能,再將效能瓶頸部分用C或組合語言改寫,達到兩全其美。

📚

利用現有生態系

許多成熟、穩定、高效的函式庫都是用C寫的。新語言可透過 FFI 直接呼叫這些函式庫,無需重造輪子。

🛠️

使用最適合的工具

不同的語言有不同的專長。例如,用Python做數據分析,用C++做圖形渲染,並讓它們在同一個專案中協同工作。