Why Some Software Is Written in Multiple Languages

2.7k 词

为什么有些软件会用多种编程语言开发?

你是否曾好奇,为什么一个软件项目有时会“混搭”使用多种编程语言?这背后其实蕴含着现代软件工程中关于性能、协作与代码复用的深刻智慧。本文将带你深入了解这背后的技术原理。
image.png

前端与后端:天然的“多语言”环境

对于我们熟悉的网站和应用,例如使用Django这类全栈框架构建的项目,其本身就是多语言协作的典型。后端逻辑可能由Python处理,而用户直接交互的前端界面则由HTML、CSS和JavaScript构建。这两部分作为独立的进程,通过网络请求等方式进行通信,各自安好。
image.png

然而,当我们需要将不同语言编写的代码编译成一个单一、高效的可执行文件时,事情就变得复杂起来。这正是本次探讨的核心。

从源代码到可执行文件:编译的艺术

要理解多语言如何“融合”,我们首先需要了解代码是如何从人类可读的文本,转变为机器可以执行的指令的。以经典的C语言为例,我们熟知的GCC编译器实际上经历了一个四步流程:image.png

  1. 预处理 (Preprocessing): 处理宏定义、头文件等。

  2. 编译 (Compilation): 将预处理后的代码转换成汇编语言。这是一个关键的中间步骤,并非直接生成机器码。

  3. 汇编 (Assembly): 汇编器将汇编代码转换成机器可以理解的二进制指令,生成 sogenannten“目标文件”(object file)。

  4. 链接 (Linking): 这是魔法发生的地方。链接器将项目中的所有目标文件,以及所需的外部库文件,整合成一个最终的可执行文件。

链接的两种方式:静态与动态

链接是实现代码复用和模块化的核心,它主要有两种方式:

  • 静态链接 (Static Linking): 想象一下,你把所有需要的工具都打包放进一个工具箱。静态链接就是将所有库函数的代码完整地复制到最终的可执行文件中。这样做的好处是程序可以独立运行,不依赖外部环境;缺点是文件体积大,且库更新时需要重新编译整个程序。

  • 动态链接 (Dynamic Linking): 这更像是你只带了一张工具清单,需要时再去仓库取。程序在编译时只记录需要哪些库(在Unix系统中是.so文件,Windows中是.dll文件),在运行时由操作系统负责加载。这种方式极大地节省了磁盘空间和内存,并且库可以独立更新,无需重新编译主程序。

GCC:不止是C语言编译器

image.png

GCC的全称是GNU编译器集合(GNU Compiler Collection),它是一个强大的工具链,天生支持多种语言,如C、C++、Fortran,甚至汇编。正是因为这个特性,开发者可以将不同语言编写的模块,只要它们能被编译成兼容的“目标文件”,就可以通过链接器组合在一起。

image.png

例如,为了追求极致的性能,像Linux内核或ffmpeg这样的项目,会把性能最敏感、最核心的部分用汇编语言编写,而其他部分则用C语言实现。这种混合编程的模式,让开发者可以在代码的可读性和运行效率之间找到最佳平衡点。

跨语言调用的秘密:ABI

然而,仅仅能生成目标文件还不够。不同语言对于如何传递数据(例如函数参数是放在哪个寄存器里)有着不同的“约定”。如果这些约定不匹配,就像两个说不同方言的人对话,会导致程序崩溃或出现无法预料的错误。

这个底层的通信规则,被称为应用程序二进制接口(Application Binary Interface, ABI)。为了让不同语言能够顺利“对话”,至少有一方需要遵循另一方的ABI。幸运的是,现代编程语言都提供了相应的工具来解决这个问题,例如:

image.png

  • C语言的 extern 关键字

  • Rust的 externno_mangle 属性

  • Fortran的 bind

  • Go语言的 import "C"

通过这些工具,我们可以明确告诉编译器:“嘿,这段代码将来要和C语言的代码一起工作,请按照C语言的ABI来生成代码。”这样,链接器就能正确地将它们“粘合”在一起。

数据存放的位置和函数预期的位置不一致

image.png

“数据存放的位置和函数预期的位置不一致”的问题。

函数A把数据存放在了寄存器 R1 和 R2,但函数B却以为数据应该在 R3 和 R4。当函数B去 R3 和 R4 取数据时,取到的是之前留下的、毫无意义的“垃圾数据”,这就会导致程序计算错误、崩溃或者出现无法预测的行为。

**应用程序二进制接口(ABI)调用约定(Calling Convention)**如此重要的原因。它就像一份统一的“交接手册”,强制规定了小明和小红必须使用同样的方式传递苹果(比如,统一规定第一个苹果必须放右手口袋,第二个必须放左手口袋),这样才能保证协作顺利进行。像extern "C"这样的关键字,作用就是告诉编译器:“请遵守C语言那本通用的交接手册来办事!”

同一个项目中使用多种编程语言

image.png

同一个项目中使用多种编程语言的原因和好处。

以下是每一点的详细说明:

  • 性能优化 (Performance Optimization): 对于性能要求极高的代码部分(例如,数值计算、图形处理、信号处理),可以使用C/C++或Rust等以速度著称的语言来实现。

  • 利用现有库或代码库 (Leverage Existing Libraries or Codebases): 可以方便地接入并使用其他语言写成的、经过长期考验的成熟库,而不必重新“造轮子”。

  • 利用系统级访问权限 (Leverage System-Level Access): 当需要与硬件、内存、操作系统底层API交互,或者有实时性要求时,某些语言能提供更底层的访问能力。

  • 加速非核心组件的开发 (Faster Development for Non-Critical Components): 使用高级语言(如Python, JavaScript)来开发用户界面(UI)、编写脚本或“胶水逻辑”,可以大大加快开发速度。

  • 团队专业分工 (Team Expertise and Division of Labor): 允许拥有不同技术专长的团队,使用他们最擅长的编程语言在同一个项目上协同工作。

  • 跨平台支持 (Cross-Platform Support): 不同的语言对不同平台(如Windows, macOS, iOS, Android)的支持度不同,选择最适合目标平台的语言可以简化开发。

  • 渐进式迁移或旧系统集成 (Gradual Migration or Legacy Integration): 可以逐步地迁移旧代码,而不是一次性重写整个系统,从而降低风险。

*总结

软件开发采用多种语言并非偶然,而是出于对性能、代码复用和利用现有生态的综合考量。通过编译器和链接器这个强大的工具链,以及统一的ABI约定,开发者可以将不同语言的优势结合起来,构建出高效、强大且可维护的复杂系统。