问题寻思

这个问题似乎很少有人提起,在互联网上找到的资料的也很老旧,但这个问题曾经有段时间一直困扰着我。

想到这个问题其实也很自然:每种编程语言的语法规则都是不同的,语言特性大相径庭,更何况他们有着各自的编译器,在编译时经历的过程也是不同的。

但是我们总是能够看到有各种各样的项目,他们是由2种甚至多种语言组成。

如果,每种语言都负责各自相对独立的模块,或许还比较能够接受,比如说javascript,python负责前端页面的设计,而C++负责后端算法和高性能功能的实现。

但是,有些功能需要的恰恰多种语言一起完成,各个语言之间的“接口”到底是怎样的呢?

多语言编程简单实例

我们先从一个简单的项目的来理解:

我们先创建一个test.cpp文件

int add(int a ,int b) {
    return a+b;
}

然后我们在bash中运行

g++ -shared -fPIC test.cpp -o test.so

这时候我们应该就能看到文件列表中出现了一个test.so

.so文件其实就是 Shared Object (共享对象文件), 它表示这个文件能够被其他文件共享,其实就是一种动态链接库

然后我们创建一个Add.py文件

from ctypes import CDLL
lib = CDLL('./test.so')
print(lib.add(3,5))

然后我们在bash运行这个文件就可以看到输出了

python3 Add.py

看到这里的时候,其实我们已经隐隐约约能够发现背后的原理了。

这里我们需要理解文件从代码到变为可直接运行的可执行文件的过程,也就是编译链接

我这篇文章已经简短地介绍了编译器和链接器的原理,大家可以通过点击链接去快速了解。

此外,我在这篇VSCODE环境配置文章开头也详细解释了代码编译过程,如果想要更深的了解可以去阅读这篇半年后的回顾文章

这里我简单说明一下,代码的编译大致经历这样的流程:

预编译 → 编译 → 汇编 → 链接 → 形成可执行文件(ELF)

绝大部分需要编译的语言在汇编过程后形成的.o文件其实已经是机器码的形式了,但是但是一个项目中往往有大量文件,而且互相依赖,而且需要系统的运行库(如.dll/.so),启动入口等等,所以他们还要经历一个过程,也即链接(Link)

在汇编完成后,所有的.o文件在二进制层面已经统一成了机器码和其他必要信息,语法层面早已不存在,这时不同语言的区别基本被抹平了

在这种情况下,文件直接互相调用几乎没有什么阻碍

而这也就是多种语言能够互相调用一起完成一个功能的底层基础:

在编译之后,只要遵循相同的调用约定,不同语言编译后的机器码就可以无缝互相调用了。

Info

我们平时敲python xx.py,看起来是直接运行,似乎不存在编译过程。其实并不是的,Python属于解释性语言(Interpreted Language),它们会通过一个名为解释器Interpreter的工具来运行。现在主流的CPython会被解释器编译为与平台无关的字节码。正因为自己不产生机器码,所以才通过ctypes,pybind11等”桥梁”和机器码交互。

所以真正的底层基础是:

所有语言最终都必须落脚到操作系统的动态链接机制和C ABI,这才是多语言互通的真正统一接口。

多语言项目的好处

也正是这种原理,我们现在流行的各种跨语言绑定工具的工具(如Pybind11, Cargo, SWIG),他们的原理其实都是通过编译文件,生成一个类似于共享库的东西,让多种语言直接实现了真正的交流(Iteraction)。

这些工具不仅方便了项目中各种语言的协调,也让开发者能够充分利用每种语言的特性。

比如:

  • 追求效率、偏向底层 → C/C++, Rust
  • 用户界面 → JavaScript
  • 高级编排、流程管理 → Python
  • 自动化脚本 → Bash

每种语言物尽其用,各司其职,让一个项目/APP能够稳定高效的运行

参考资料

  1. How Multiple Programming Languages Work Together in a Single Project
  2. (Video) Why Some Projects Use Multiple Programming Languages

Leave a comment