编程语言中引入外部代码、构建以及包管理机制的一些理解

前言

前一段时间和同学讨论cpp如何引入第三方代码的事儿,涉及到一些模块化和包管理机制的内容。关于这部分的内容还不甚了解,以前就想做研究一下,就利用这个月的博客做个总结,希望可以便人便己。


一、引入外部代码的方式

在这里插入图片描述

在编写大型项目的时候,如果标准库提供的功能无法满足项目需求,一般来说都会在项目中引入第三方提供的库。不同语言在引入外部库时采取的方式各有不同,但总结一下大概有以下两种:

  1. 以头文件、源代码的方式进行引入
  2. 以二进制、库文件的方式(包括linux上.o的目标文件和java上的.class文件)引入

下面以cpp、java 和go语言为例分析一波。

1.1 C/Cpp 中引入外部代码的方式

在这里插入图片描述

对于C和Cpp来说,在语言层次上有头文件和源文件的概念,前者负责符号的声明,后者负责符号的定义。在那个古老的年代,最初设计这种方式是因为当时的内存比较小(大概只有几十kb),编译时源码文件无法完全放入内存中,所以将符号的声明放入头文件中,把较大的源码文件按照模块化编译的原则分成多个编译单元(多个源码文件)进行单独编译,最后链接成一个可执行文件。 以信息屏蔽的视角来看,将声明和定义分开,也符合“暴露接口,隐藏细节”的原则。

在引入外部代码时,使用#include头文件的方式,将外部代码的相关声明包含在整个编译单元之中。预处理阶段将会把外部代码的声明以类似文本拷贝的方式复制到目标文件中。

如果库以“Header Only”的方式提供,那么在程序构建时,被引入的外部代码也会参与整个编译过程。这就是上面提到的方式1。

如果库除了头文件之外,还有另外的源文件,编译阶段依然会用到引入的头文件,除此之外,在链接的时候一般需要指定外部源代码编译成的二进制文件(.o目标文件或者静态、动态库文件)。这就是上面提到的方式2。

这里顺便在提一句,有的时候我们需要在linux上install软件的时候(比如说openvpn),除了编译安装openvpn本身的代码之外,还需要额外安装一些依赖的库,比如说openssl(否则就会发生各种链接错误), 其中的缘由就是上面所说的。

当然还有另外一种方式,就是把外部库的源文件和头文件一起搞下来,merge到本地的项目中,让其成为本地项目的一部分参与构建。这其实也属于上述的方式1了,但是这种方式对于小型工程来说还行,大型项目的编译时间和环境配置上可能就略显吃不消了。

1.1.1 关于cpp 20中的modules

C/CPP中使用头文件的方式有一些经常吐槽的缺点,其中重要的一点就是降低构建的速度。如果a.h 每被include一次,在预处理阶段就会被展开一次,之后膨胀的源文件在编译过程中就会让人难受的想吐。在新的cpp20的标准制定中,添加了一个新的特性-modules机制。使用了modules机制之后,在引入外部代码的时候就可以像java和go中那样,直接import,而不需要include头文件了。

对于cpp来说,还有一个槽点是cpp一直缺一个统一好用的包管理工具,其中的一个原因是使用include引入头文件的方式不利用做cpp库(包)和组件的管理工具。有了moules之后,情况应该会慢慢改善。

我没用过这个新的modules机制,但是网上不少人说这个modules机制可以提高编译的效率。但是cpp的复杂繁多的构建系统,让这个modules正式落地成熟应该还有不少路要走。

1.2 Java中引入外部代码的方式

在这里插入图片描述

在java中使用库来组织函数、数据和类,如果想引入第三方类库,直接import对应的库即可,这些库是以jar包的形式提供的,在使用时需要准备好对应的jar包。这些jar包里是用第三方源代码编译生成的.class文件,这种文件从某称程度上说也应该算是编译后的中间文件,可以归属到上述的第二种方式。


这里稍微说点题外话。
请问,java是编译型语言还是解释性语言?

答:按照我的理解,它应该即算是编译型也算是解释型的语言。从源代码到.class文件这个过程是通过编译达到的,从.class到目标机器可以执行的二进制代码是使用java虚拟机来进行解释的。

1.3 Go语言中引入外部代码的方式

在这里插入图片描述

和java类似,go中导入第三方包(库)也是使用import,不同的是Go语言中引入的包主要是通过第三方源码的形式引入的,即第三方源码会与本地代码一起编译成最终的二进制程序。也正是因为采用源码库的方式引用第三方代码, 对于一般go语言程序来说,重新编译就像是吃饭喝水,普通得不能再普通。对应于开头所述,Go引入外部代码采取的方式属于上述的方式2。为了解决编译速度问题,Go语言在设计时也做了不少权衡,如为了加快编译速度不允许循环引用。

当然,准确的说,Go也可以使用二进制的方式,通过编译成静态库的方式分发代码,但是这种方式用的比较少,这里就不多提了。

二、构建和包管理机制

所谓构建指的是从源码开始到成为二进制可执行程序的过程。而包管理机制在我理解就是处理整个工程项目的库依赖管理、版本控制并且和构建工具结合,更加方便的编译成可执行程序。 对于90年代之后出来的语言,比如说java、go、javascript来说,它们都有比较好的构建和包管理机制,而对于C/Cpp这种略显古老的编程语言来说,目前还没有成熟统一的构建和包管理机制。

下面依然以Cpp、Java和Go为例,简要介绍它们谈谈的构建和包管理机制。

2.1 Cpp的构建和包管理机制

在这里插入图片描述

由于Cpp是较为贴近底层的编程语言,其构建过程和操作系统、机器架构(x86,arm等)都有关系,再加上不统一的编译器、没有模块机制等,使得其构建和包管理机制都变得比较复杂。

目前来说,Cpp的主流编译器主要有GCC、Clang和MSVC。对于比较小的demo,可以使用手动编译的方式进行构建;

对于稍大的系统,需要使用专门的构建工具,目前主要的构建系统有Make,GNU Autotools、ninja。 用的比较多的应该算是Make了。根据Make的规则,编写Makefile文件,最后来一步Make,自动执行整个构建过程;

当然,Makefile的编写也略显复杂,机械且容易出错,尤其是对于依赖较为复杂的工程项目来说。为了简化Makefile的编写的同时适配多种平台架构,又有前辈开发出来了元构建系统(meta build system)来帮助用户编写Makefile。元构建系统不会执行构建操作,而是从一个较高的层次((如CMake在CMakeLists.txt文本中)描述构建依赖关系,并转化成Makefile等底层的构建系统。一般来说,元构建系统会屏蔽掉平台相关依赖,这样元构建系统具有极好的跨平台特性。 目前主流的源构建系统由CMake、gn、QMake等。(按照我的接触范围来说,CMake用的较多些)

以上说的是Cpp的构建过程,对于包管理工具来说,由于众所周知的原因,Cpp目前并没有统一的包管理机制,目前使用较多的有cnona、vcpkg等,但是其各自都还有些问题,还未形成统一市场的格局。详细的包管理在此就不赘述了,有兴趣的看客可以参见【7】。

2.2 Java的构建和包管理机制

在这里插入图片描述
一般来说,手动构建Java项目的比较少,由于Java的构建生态做的很好,在实际使用时一般都是基于项目采用专门的构建工具和包管理机制。 Ant和Maven都是基于Java的构建(build)工具,有点类似于上述的Make。Ant是软件构建工具,Maven的定位是软件项目管理和理解工具。

enen…, 太细的我也就不是很懂了╮(╯▽╰)╭。

2.3 Go的构建和包管理机制

在这里插入图片描述

作为新时代的C语言,Go在设计之初的构建和包管理机制就不像Cpp那样麻烦和复杂(虽然一开始做的不太好)。由于google作为唯一指定官方,所以其包管理机制和语言本身贴合的更近(虽然包管理工具是外部的)。 按照网上的说法,其构建和包管理机制大概经历了三个阶段:

  • GOPATH阶段
    这种机制所有的包都放在GOPATH/src路径下,使用go get 可以将想要的包下载到GOPATH路径下。在项目启动时,会根据 import 自动获取对应包,但是不支持区分版本,无法进行有效的版本管理。

  • Go Vender阶段
    在Go1.5 版本中引入govender,在Go1.7中强制启动。它基于 vendor 机制实现的 Go 包依赖管理命令行工具。它分割了不同项目对同一个外部库的依赖,有效的解决了不同工程使用自己独立的依赖包问题。在编译时搜索路径首先从/vendor开始,然后是GOPATH,最后是GOROOT。

    但是这种方式造成了一定程度上的冗余,同一个库如果在不同工程项目使用需要另外拷贝一份到对应的/vendor下。

  • Go Modules阶段
    在Go1.11 之后,官方退出了Go Modules机制,这个机制中所有的依赖包放在GOPATH/pkg/mod目录下,生成的二进制文件放在GOPATH/bin目录下。整个工程项目可以独立于GOPATH,放在其他的地方,但项目中需要有go.mod文件来描述一些版本信息。

    和第一种方式GOPATH类似,这种方式也指定了依赖包的位置,但是由于go.mod中记录了项目使用外部依赖的版本,所以既实现了工程之间重用依赖包,也解决了不同项目对同一个外部包的不同版本依赖问题。

    现在主要是使用这种方式来进行包管理。

后记

好,最后问个问题?

如果说 java js这些是运行在虚拟机之上,可以提供方便的包管理机制,那么go没有运行在虚拟机中,为什么也可以?为什么cpp没有一个好的机制呢?

你觉得呢?


参考

【1】C++20 四大特性之一:Module 特性详解
【2】c++如何使用别人写好的库?也要用include命令把库文件包含进来吗?
【3】C++20 要来了!
【4】go与java的依赖注入实现的一些差异
【5】为什么同为系统级编程语言,Rust 能拥有现代构建/包管理工具,C++ 却不能?
【6】C/C++构建系统简介
【7】打包一沓开源的 C/C++ 包管理工具送给你!
【8】Ant和Maven的区别
【9】Golang 的包管理 —— go module
【10】go的三种包管理方式
【11】C++20 - 下一个大版本功能确定
【12】C++20 新特性: modules 及实现现状
【13】C++20的模块系统
【14】C、C++、Java、Python、Go、Rust、Dart 头文件、库、包、模块
【15】Go:包管理工具GOPATH、vendor、dep 、go module

猜你喜欢

转载自blog.csdn.net/plm199513100/article/details/124567840
今日推荐