欢迎光临散文网 会员登陆 & 注册

《Makefile 光学教程》之面向 Makefile 编程·C/C++ 项目模板 [Glib-2.0 & ADT]

2023-09-23 22:17 作者:紧果呗  | 我要投稿

此教程将计划以两部分内容呈现,目标是从零基础到 GNU make 最本原的原理的掌握,这是第二部分内容,按不同的工程类型分成多个示范项目来展示。零基础可以先看第一部分:Basic Concepts:

1.  🐣 Basic Concepts

2.  🐣 Demo Projects

    2.1.  🐣 Scheme R6RS 语言规范文档处理 [LaTeX]

    2.2.  🐣 Multi threaded Download [Msys2 Packages]

    2.3.  🐣 C/C++ Project Templates [GLib Gobject & ADT]

    2.4.  🐣 Erlang Project Templates 

    2.5.  🐣 Unit Test [CPL] 


此部分涉及内容较多,主要是 OOP 思想与 GLib (GObject)框架内容,将细分为三小节,内容较多请忍耐一下:


  1. 📜 GLib–2.0 前置教程:Msys + Meson 构建工具

  2. 📜 GLib–2.0 GObject ADT 类型系统库

  3. 📜 GObject–2.0 OOP 框架入门教程

完整《Makefile 光学教程》以及 GNU M4 教程参考开源文档:https://github.com/Jeangowhy/opendocs/blob/main/Makefile.md

📜 GLib–2.0 前置教程:Msys + Meson 构建工具

研究开源库过程中养成了一个不知是好是坏的习惯(自觉更能进入心流状态),那就是首先分析源代码中的开源文档结构。通常,官方文档是研究开源库的第一手资料,其次是搜索引擎能找到的优质资料,之所以强制优质,是因为现代社会制造垃圾信息的成本太低了。换个“不恰当”的说法就是:造谣一张嘴,辟谣跑断腿!


可以从 gitlab.gnome.org 上下载到 glib2 框架源代码,感谢 GTK+ 开发团队提供高质量的开源文档。源代码使用基于 Python 3 + Ninja 组合实现的 meson 作为构建工具,默认以 meson.build 为配置脚本,Meson Build system 工作模式类似 CMake:


理解 Makefile 基本原理后(依赖关系网络),学习 CMake 或者 Meson 这些高级自动化构建工具就易如反掌。Meson sample 提供的 meson.build 脚本示范参考,一眼就可以看到它们隐含的依赖关系处理,子目录下的 `meson.build` 脚本只需要调用 `subdir('gio')` 这样的函数就可以嵌套处理 :

以下 meson.build 示例来自 Meson IndepthTutorial.md 文档,演示如何使用 pkg-config 查找依赖库:


CMake 和 Meson 都是非常现代的自动构建工具,都是值得学习的自动化构建工具,前者使用 C++ 实现,代码量较后者多几倍。由于 Meson 基于 Python 之上构建,所以节省了一定的代码量。双方都有非常完善的文档,CMake 文档使用 reStructured Text 格式,内容非常精细,甚至可以用繁多来形容,具体到每个变量、每个函数都有一个文档对应,当然也有目录。Meson 使用 Markdown 用户指南加 YAML 参考手册格式,它们都是非常好用的文档格式,和 markdown,或者专业排版的 TeX 或者 LaTeX 都是非常优秀的开源文档格式。


它们两者本身就是一个 DSL 领域特定语言,专用于处理构建过程中的依赖关系、依赖库处理等问题。甚至可以将二者的源代码作为研究编译实现的范本项目:

Meson 为了降低自身出现依赖问题,约定一条规则:不使用 Python 基础标准库以外的模块,只需要 Pyton 3 和 Ninja。Ninja 使用 C++ 实现极轻量的构建工具,其设计目标之一是“必须易于嵌入大型构建系统”。Ninja 的规则文件 ninja.build 并没有条件语句或是基于文件后缀的规则,相反,使用列表记录确切的输入文件路径,以及所产生的确切结果。因为这种简单的表达并不需要额外的解释,所以,在运行时,这些规则文件能够被快速载入。由于 Ninja 追求目标简洁,就像是一个新式的 GNU Make,它没有隐式规则、没有函数、也没有第三方依赖,源文件不到 1MB,使用 CMake 就可以执行编译。注意,Msys2 编译环境有可能出现 ‘Subprocess’ 类型字段没有定义的错误,不能通过编译:


Meson 为非原生构建的项目提供 Wrap database 服务,项目中可以使用 .warp 文件提供模块信息,其功能类似 pkg-config 中使用的 .pc 文件。可以使用 `meson wrap` 命令进行查询、安装等等操作。


Meson 支持多种依赖库配置工具,可以在其依赖对象中 method 设置中指定:默认值是 `auto`,可选择使用 `pkg-config`, `config-tool`, `cmake`, `builtin`, `system`, `sysconfig`, `qmake`, `extraframework` 还有 `dub`。默认的依赖库查找控以下顺序处理:


  1. `pkg-config`

  2. `cmake`

  3. `extraframework` (OSX only)


Meson 官方文档自信满满,各项指标都暴打 GNU Autotools 这套臃肿的自动化构建工具。作为后来都,Meson 还支持将 CMake 项目作为子项目导入。作为 GNU Autotools 的反面,GNU Make 真正做到小而美,它在实现上的克制(绝对不乱加代码实现混乱的功能)使用得 GNU Make 始终是自动化构建工具的典范!当然,现代的自动化构建工具,已经不需要开发者手写 Makefile 脚本了,很多规则定义工作只需要 CMake 或者 Meson 的一个函数就可以替代,包括代码文件的生成,但是 GNU Make 传承下来的依赖处理的理念始终是根本。


Meson Build system Features


*   multiplatform support for Linux, macOS, Windows, GCC, Clang, Visual Studio and others

*   supported languages include C, C++, D, Fortran, Java, Rust

*   build definitions in a very readable and user friendly non-Turing complete DSL

*   cross compilation for many operating systems as well as bare metal

*   optimized for extremely fast full and incremental builds without sacrificing correctness

*   built-in multiplatform dependency provider that works together with distro packages

*   fun!


Meson 文档目录记录在 docs\sitemap.txt 文件。

https://github.com/mesonbuild/meson/blob/master/docs/sitemap.txt


Meson 文档使用 YAML 格式
CMake 文档使用 reStructured Text 格式

C/C++ 代码阅读推荐使用 Sublime Text + LSP 插件 + Clangd LSP 服务器:

LSP: Settings 中设置 Clangd 编译器提供的 C/C++ LSP 语言服务:

Sublime Text + LSP = 熟码神器


Meson 文档 Vala.md 展示了 Gnome 为了简化基于 GLib 的图形应用程序而开发的 Vala 和 Genie 编程语言项目。Vala 支持现代语言特性,借鉴了大量的 C# 语法。而发行在两年后的 Genie 则参考了 Python 和 Delphi 语言,但是它们都使用相同的 `valac` 编译器(转译器),.vala .gs .vapi 等代码文件会转换成 C 语言代码,再编译成二进制程序执行。


https://wiki.gnome.org/Projects/Genie?action=AttachFile&do=get&target=genie_and_valac.svg

Vala 是一门新兴的编程语言,由 GNOME 主导开发,支持很多现代语言特性,借鉴了大量的 C# 语法,Python 的手感,C 的执行速度,Vala 最终会转换为 C 语言,然后把 C 代码编译为二进制文件,使用 Vala 编写应用程序和直接使用 C 语言编写应用程序,运行效率是一样的,但是 Vala 相比 C 语言更加容易,可以快速编写和维护



GLib 官方网站上的文档都可以在源代码中找到对应的 xml 源文件,分别可以在以下三个 meson.build 脚本中找到对应的目录:


1. glib-2.78.0\docs\reference\gio\meson.build

2. glib-2.78.0\docs\reference\glib\meson.build

3. glib-2.78.0\docs\reference\gobject\meson.build


GLib 框架文档列表中包含: glib-docs.xml 即 GLib API 文档目录文件;gobject-docs.xml  即 GObject 模块 API 文档目录文件; gio-docs.xml 即 GIO 模块 API 文档目录文件。

入门应该先读 GObject 教程部分,即 tut_intro 入门教程。以下 GnomeVFS Overview 架构图可以帮助理解 GLib 的大体结构。Virtual File System (VFS) 即构建于内存空间的文件系统,相对于传统磁盘中的文件系统。


GnomeVFS Overview  https://docs.gtk.org/gio/gvfs-overview.png

Msys2 平台中使用 pacman 安装依赖库,包括安装 pkg-config 依赖库信息管理工具(使用 pkgconf 作为其兼容实现):

1. https://packages.msys2.org/base/pkgconf

2. https://packages.msys2.org/base/mingw-w64-pkg-config




📜 GLib–2.0 GObject ADT 类型系统库


Gobject 即 GTK 为 C 语言提供类型系统实现而开发的 Glib 基础库的扩展,用于辅助 C 语言编写面向对象程序,提供以下内容:


1. 一个通用的动态类型系统(GType)

2. 一个基本类型的实现集(如整型、枚举等)

3. 一个基本对象类型 Gobject

4. 一个信号系统以及一个可扩展的参数/变量体系。


GObject 基于 Glib 实现动态类型系统 GType,原来是 GTK+ 的一部分,GTK+ 2.0 中将与 GUI 不相关的部份都移到 GObject 而创建了此类库,源码包含在 Glib。gobject-query 命令可以用来查询类型树。

GObject 世界里,一个类类型定义是*实例结构体* GObject 和*类结构体* GObjectClass 两个者的组合。GObject 的继承机制需要实现实例结构体的继承和类结构体的继承,Gobject 对象的初始化可分为两个部分:类结构体初始化、实例结构体初始化。类结构体初始化函数只被调用一次,而实例结构体的初始化函数的调用次数等于对象实例化的次数。这意味着,所有对象共享的数据,可保存在类结构体中,而所有对象私有的数据,则保存在实例结构体中。为每一个对象分配一个 ID,即 GType 这个用于标识类型的值,使用引用计数方式进行内存管理。


GLib 可谓 C 语言中的“STL”,在此之前,动态数组、链表、哈希表等通用容器,可能每个 C 开发者实现过 N 次以上。甚至在同一个项目里,出现几份链表的实现,也并非罕见。GLib 的开放终结了重复造轮子的恶梦。GLib 提供动态数组、单/双向链表、哈希表、多叉树、平衡二叉树、字符串等常用容器。完全面向对象设计,完全跨平台,通用的 set/get 属性访问,内部实现信号机制(本质是 Observer Design Pattern)。


GStreamer 就是一个基于 GLib 构建的通用流媒体应用程序开发框架,GStreamer 最显著的用途是在构建一个播放器上,支持多种格式,包括: MP3、Ogg/Vorbis. MPEG-12、AVI、Quickime、mod 等等。


Geany 是基于 GTK+ GLib 实现的一个轻量快速的 IDE,集成了语法高亮、命令自定义、项目构建功能以及插件扩展,可以实现 Make 等外部功能集成,基本上达到轻量与快速的目标。但是远达不好好用的级别,界面设计还是停留在传统的区域分割设计,强制需要鼠标点点点(鼠标手警告)。和 Sublime Text 不在同一级别,只能和 Notepad 或 Editplus 相比较,但也打不过人家小巧可爱。 

https://zenlayer.dl.sourceforge.net/project/notepadplusplus.mirror/v8.5.7/npp.8.5.7.Installer.x64.exe


基于 GLib OOP 程序开发涉及以下方面的内容:

1. GObject instantiation

2. GObject properties (set/get)

3. GObject casting

4. GObject referencing/dereferencing

5. glib memory management

6. glib signals and callbacks

7. glib main loop

本文篇幅受限,不能涉及所有的内容,只限于类型系统基本原理、和 GObject 基本使用。


面向对象编程本质上是人类抽象能力集中体现,计算机编程中一切数据类型都是抽象概念。比如说,整数、浮点数它们真实存在计算机系统内吗?其实没有。它们基于人类构建出来用于表达抽象概念的机械之上才得以呈现。同样的,高级语言中的函数、类方法等等,都是抽象而来的概念,本质上它们都是 CPU 控制数据总线从磁盘加载到内存中的一段具有典型特征的代码,这些特征包括:使用 push 以及 call 指令,在返回的位置调用 pop 指令。


抽象”这一概念的最佳说明就是毕加索的《公牛》,全画几乎就是用了少量简单的线条完全概括出牛的生物结构。这副画并不是一次画成的,而是从具象的牛慢慢地,经过多次演绎才演变为最终版本的极简牛!《公牛》画作创作时间从 1945年12月5日到1946年1月17日完稿,长达 6 周有余。


笔加锁的公牛 [Doge]

说明抽象这一概念的另一个例子是数学,一个苹果和另一苹果,一个绳结和另一个绳结,这些都是具象,这些都在人数数学诞生前计数的概念,当一个苹果成为 1,一个绳结也成为 1 之后,两个苹果或者两个绳结就是 1+1=2,数学就这样诞生了!而 1、2 和 + 都是数学符号,= 号是约定规则符号。


抽象就是要教会我们抓住研究对象最本质的东西,通过概括完成对复杂的事物进行系统的梳理,这就是少即是多的哲理。抽象是共通于生活、编程、艺术等等领域共通的基本能力。


从抽象出发,OOP 中的类形这一概念就是对一切可能的数据结构的高度概括,类定义就可以看到是这一概念具象化的第一步,*类型实例化*则是这一概念具象化的下一步,最后*类实例化*完成了抽像概念的最终具象。这个过程就像是从抽象的牛到各种品种的牛,再到某人家的牛,从概念到具象的过程。


实现“类型实例”就是在创建更多的 "Class",而“类实例化”就创建更多某种类型的具象 "Instance of Class"。这个描述可能有点拗口或混乱,换个说法就是“type instances”和“class instances”的区别。在编程中,`Type` 和 `Class` 是两个经常用到的术语,当使用 Type 时通常是指高度抽象的类型,使用 Class 则是指经过一轮具象处理的类型,就像从“牛”到“奶牛”这一过程。具象化即实例化,对抽象类型进行具象化就是具体的类型,对具体类型的具象化就是类实例。在实际的编程工作中,主要关心的是使用 `class` 关键字定义类型,使用 `new` 关键字实例化这个类型得到一个具体的“对象”。OOP 中最令人迷惑的术语大概就是 Object 一词,这个问题在 JavaScript 的实现中尤甚。


动态类型语言中,典型代表有 JavaScript、TypeScript、Python、PHP 等等,这此语言更多的是使用 duck typed,即叫起来像鸭子、走起路来也像鸭子、长得也像鸭子,那么就可以认它是鸭子。这是一种生物学人类思想,是动态类型语言的基本类型实现逻辑:dynamically typed。


TypeScript 示范代码如下,注意花括号是 JavaScript 中的对象字面类的表达形式。可以进行 duck = darkDuck 这样的赋值,因为 darkDuck 拥有 duck 的所有特性(这里指 gaga),相当于 C++ 继承类型系统中的子类型。返过来,并不能将 duck 赋值给 darkDuck,因为它缺失部分兼容的特性:



原生类型可以认为是只有数据的对像的抽象结构(char, int, long, float, double),而复杂类型可以认为是除了数据,还封装了相应接口方法的抽象结构。C++ 入门课程一般都会学习 Abstract Data Types (ADT) 概念,通常指的是复杂的类型 (Lists, Sets, and Maps),但是在我看来,编程中涉及的所有数据类型都是抽象数据类型,只是复杂程度不一样。


另外,在中文编程教材中经常会出现一个词“堆栈”,比如说堆栈溢出导致程序崩溃。其中栈和堆对应 Stack & Heap,是程序装入内存后运行中需要使用的两块内在区域。Heap 单词本来指一些东西凌乱地堆在一起的状态,Stack 单词同样也指一些东西堆在一起,但是整齐堆叠在一起。堆内存相对较大,可以是操作系统中所有未使用的内存空间,而栈内存相对较小,通常在程序运行时配置其大小,比如说 10MB。


Stack 是一种 FIrst-in Last-Out (FILO) 或者 Last-in First-Out (LIFO) 数据结构,它的特别之处在于:CPU 硬件内置了一个 Stack Pointer (SP) 栈内存指针寄存器。另一个重要的寄存器是 Program Counter (PC) 通用寄存器,用来指向程序要运行的下一条指令的地址。程序执行时,每当调用函数就会使用 push 指令在 Stack 内存中规则地存入参数和返回上层函数的地址,函数返回时则使用 pop 指令将相应传入的数据从 Stack 中移除(回收 Stack 内存)。所以这些有限的 Stack memory 总会有可能出现耗尽的时候,递归函数调用经常会导致堆栈溢出问题。


回到 GLib OOP 框架,GObject 则是意图呈现上面所述的抽象过程,开发者从这个抽象(GTypeInstance 和 GTypeClass)演化出更多其它类型的实现,最终用户对这些构建出来的类型进行实例化并使用它。


C 语言本身发布比较早,1971 年的时候还没有 OOP 编程的语法规范,所以在 C 上使用 OOP 编程思想,就是直接定义函数作用类型对象的 API 方法。本质上,函数就是内存上的一段代码,根据 C 语言调用函数规则以及函数地址,就可以在外部(其它语言)调用 C 语言实现程序中导出的函数。比如,Python、Erlang、JavaScript (WebAssembly) 或者 PHP 等等,这种语言间的互调用 (interoperability Interprogramming) 最能体现 C 语言作为底层语言的强大。


以下是一段 C 语言程序代码,以及调用静态函数时对应的汇编指令。静态函数就是 C/C++ 中一个处理单元(一个源代码文件)可以访问的函数。



函数的调用约定 calling conventions 包含参数的入栈顺序,对寄存器也有影响,以 x86 cdecl,即 C 语言函数的调用约定为例:

1 - 函数参数通过栈传递,参数列表按从右到左顺序压入栈内存,并且由调用者负责清理栈中的参数。

2 - 整型值和内存地址通过 EAX 返回。

3 - EAX, ECX, EDX 由调用者负责保存,其余的由被调函数负责保存。


C/C++ 默认使用 `__cdecl` 调用约定,由于此方式由主调函数负责参数入栈、清栈,所以可以实现 `vararg` 即变参函数,参数列表使用 `va_list` 引用。

微软官方声明 `__pascal`, `__fortran`, `__syscall` 等为过时约定不再支持,参考下表https://learn.microsoft.com/en-us/cpp/cpp/calling-conventions


假设,要通过 Python 调用以上定义了静态函数,那么就需要按以下步骤操作:


1. 在 C 语言编译器生成的程序中找到导出函数的地址;

2. 加载可执行内存(操作系统根据程序代码需求分类内存)中的函数代码;

3. Python 将参数转换为兼容 C 语言的类型;

4. 按照 C 语言调用约定 `__cdecl` 处理参数入栈并调用函数;

5. 将返回值转换回 Pythong 的数据类型;


Foreign function interface (FFI) 就是用来表示这种互调用的术语,这两种语言中间进行数据转换的代码称为胶水代码 glue code。


参考 Python-3.11.0 源代码中的文档:

1. Doc\c-api\abstract.rst               Abstract Objects Layer

2. Doc\library\ctypes.rst                A foreign function library for Python

3. Doc\extending\embedding.rst   Extending and Embedding the Python Interpreter


胶水代码可以手写,为每一个导出函数手写脱水代码是劳动生产效率最低的方式。另一种更高效的方式是使用专用编译器,根据导出函数的签名自动生成相应的脱水代码。


GType 的解决方案是使用通用胶水代码,其最大优点是:位于运行时域边界的胶水代码只写一次,下图更清楚地说明了这一点。


语言间互调用的胶水代码

目前,存在多个通用的粘合代码,这使得可以在各种语言中直接使用 GType 编写的 C 对象,只需最少的工作量:不需要自动或手动生成大量的粘合代码。GType/GObject 的设计不仅是为了向 C 程序员提供类似 OOP 功能,而且是透明的跨语言互操作性。



📜 GObject–2.0 OOP 框架入门教程


设计类型时就需要考虑类名选取、继承链信息、类型初始化顺序、类接口方法设计等信息,这些基本概念涉及到的主要源代码文件如下:


0. 使用 GLib 框架需要引用 glib.h 头文件;

0. 使用 GObject 框架需要引用 glib-object.h 头文件;

1. `GObject` 各个结构声明在 gtype.h 文件;

2. `GObject` 各个函数声明在 gobject.h 文件;


源代码包含了丰富的注解内容,一般函数命名规律是:以相关对象名称为前缀。比如 GObject 对象的相关函数就有 getv 或 setv,完整名称就是 `g_object_getv` 和 `g_object_setv`。文档包含了一个符号列表文件,所有 GObject 模块中定义的宏符号、函数都分类罗列在: glib-2.78.0\docs\reference\gobject\gobject-sections.txt


`GObject` 各个结构声明在 gtype.h 文件,注意对位关系,逻辑说明如下:


 * `GObject` 结构定义的所有字段都为私有,类型实现者不该直接访问;

 * `GObjectClass` 为类型实例提供构建、析构、属性访问、消息机制等接口机制;

 * `GTypeInstance` 内部结构,表示类型实例的基础结构;

 * `GTypeClass` 内部结构,表示类型基础结构,Basic Type Structures;

 * `GType` 是一个用于标识各种类型实例数值,即 Type ID;

 * `GTypeInterface` 是所有接口类型的基础结构;

 * `TypeNode` 是记录类型关系数据的节点树中节点结构;

 * `GTypeQuery` 是用于记录类型信息的结构;

 * `GTypeInfo` 和 `GTypeFundamentalInfo`,以及 `GInterfaceInfo` 是记录类型信息的结构;

 * `GTypeFundamentalFlags` 和 `GTypeFlags` 枚举值用于控制不同类型的功能特性;


一般地,64-bit 环境下,`GObject` 类型占据 24 字节:两个指针加一个 unsigned int 用于记录引用计数。



假定要派生一个 GObject 子类型,即定义一个类型通常需要将父类型基础结构重叠(boilerplate)到子类型的结构中,并且需要作为前缀字段定义,这样在进行向上转型(转换为父类型)就可以通过原指针获取到父类型的结构:


1. 一个包含 `GObject` 的结构体作为类型实例的本体,顶级类使用 `GTypeInstance`;

2. 一个包含 `GObjectClass` 的结构体作为类型本体;


参考 `g_type_module_get_type` 方法的实现,内部的 `GTypeModuel` 就是这样的一个子类型。简单概括为一个嵌套结构,类型信息结构则会传递给注册函数以登记到类型树数据系统中:



从三个最基本的信息记录用途的结构: `GTypeFundamentalInfo` 和 `GTypeInfo`,以及 `GInterfaceInfo` 就可以发现,GLib 类型系统中的基础类型、一般类型以及接口类型所有具有的基本功能。结合 Flags 控制位,开启或关闭某种特性,比如是否可被继承、是否可以实例化、是否是 Final 类型等等。


Glib 并没有提供 Flags 枚举值来指定是不否是静态类型,而是提供了多个注册函数,来区别静态类型、基础类型、一般类型和接口类型。


创建一般用户类型时,`GTypeInfo` 必不可少,注册基础类型时还额外增了 `GTypeFundamentalInfo`。基本类型信息记录包括 :


1. `class_size`: class size.

2. `base_init` and `class_init`: class initialization functions (C++ constructor).

3. `base_finalize` and `class_finalize`: class destruction functions (C++ destructor).

4. `instance_size`: instance size (C++ parameter to new).

5. `n_preallocs`: instantiation policy (C++ type of new operator).

6. `value_table`: copy functions (C++ copy operators).

7. type characteristic flags: `GTypeFlags`. 


可以从 `GTypeInfo` 结构中看到,设置有 base 和 class 两对初始化、终止方法,而对于一个类的实例则只设置一个初始化方法。base 相关的两个方法一般用不上,它可以在类型初始化之前,或类型析构之后做一些工作。


一个几乎没有任何用途(除了测试编译流程)的示范程序如下:



GLib 2.36 版本后,已经不需要主动调用 `g_type_init` 方法,GLib 系统自动初始化类型系统,而这个函数什么也不做。

Windows 系统上使用 Msys2 环境开发 GLib 应用,以下命令用于检测环境要求,以及编译源代码,以供参考:


可能遇到的问题:

最后,还有一个潜在问题,Msys2 使用的编译器平台不兼容,导致编译出来的程序出现非法指针,内存违规访问。这种情况会导致 bash 不能检测到返回码,-1073741819 0xC0000005 STATUS_ACCESS_VIOLATION。错误码参考 2.3.1 NTSTATUS Values。


Msys2 bash 环境中出现引用未定义符号;

PowerShell 环境下找不到头文件或者库文件,是因为命令行没有正确传递头文件目录,以及依赖库名称中包含句点,这会导致编译器误判文件名。应该使用引号包括依赖库 `-l"gobject-2.0" -l"glib-2.0"`。


Msys2 中的依赖包默认使用 /usr 此类绝路径前缀,pkg-config 可以根据 PKG_CONFIG_SYSROOT_DIR 环境变量指定的路径替换依赖包 .pc 文件中的 $(prefix) 路径。


另外,就是 PowerShell 中使用变量、或者同命令行中传递 pkg-config 返回的编译器参数时,会因为自动给变量加引号面导致参数失效(所有配置项变成一个字符串参数)。此情形不能直接使用 -split 进行字符串分割,PowerShell 使用反引号作为转义符号,也不能像 bash 一样使用反引号插入其它命令。解决方法有多种:


1. 使用新 pwsh 进程的 -c 选项解释命令行(恶心)。

2. Invoke-Expression 执行字符串包含的命令及参数(依然有 glib-2.0 名称问题)。

3. 最佳方法是 Make 或者 CMake、Meson 等构建工具,可以很好处理这样的问题。

4. 终极的解决方法是:手动给 GCC 指定参数或者修改 GLib 依赖库名称和 .pc 配置。


PowerShell 调用运算符 (&) 用于执行脚本、脚本块或命令,但它不会像 Invoke-Expression 那样解释 command 参数,它会将字符串当作一个命令(不含要传递的参数)。


比较棘手的是引用符号未定义,这是高发编译错误,解决难度罗列:


1. 没有给链接程序指定链接库,比如缺失 `"-lglib-2.0"` 或者路径错误;

2. GCC 链接库参数 -l 不要写在源代码文件之前,会链接不到库;

3. 指定依赖库依然报错,此情况就可能是使用了不兼容编译器版本;

4. 最后,是真得找不到相应的符号,可能是代码写错或者库中没有定义;


GCC 链接器链接文件时的流程:


1. 从左往右链接源文件;

2. 源文件调用了没有定义的函数、变量等,则记录下来;

3. 如果遇到 -l 指定的库,则在库中尽可能检索所有记录下来的没有定义的函数、变量,只从库中提取用到的部分,然后抛弃此库;

4. 在全部链接完后,如果依然存在未定义的函数、变量,则报错


正因为 GCC 链接器的这样的链接流程,并不会回过头来检索之前链接过的库,因此要求按正确顺序传递依赖库。由此可知,即使两个库有相同的函数、变量的定义,最终只有先找到的库中定义的符号才生效。

以下是 meson.build 脚本用 Meson 构建命令参考(glib-2.0 依赖可以省略,因为 gobject-2.0 已经包含):


很难下结论说,到底 CMake、Meson 这类功能丰富的自动化构建功能好(甚至支持 CI/CD 功能),还是说 GNU Make 这种经典的小可爱好。双方都有优点和缺点。功能丰富意味着,学习它需要涉及更多的概念(心智负担、学习曲线),但是经过优化,有些功能可以做到开箱即用。比如说 Unit tests,可以在 meson.build 脚本中调用 `test` 函数设置需要进行单元测试的程序,它可以接收 `excutable` 函数返回的 exe 对象。如果事先不知道这一点,那么就会对编脚本中返回的 exe 对象形成抽象概念理解的困难。

Meson 单元测试使用 `meson test` or `meson test "*hello"` 等命令形式调用,生成的报告会保存在 `meson-logs/testlog.txt`。



使用传统的 Make 脚本,由于它功能特性保持稳定,一旦掌握就不需要消耗更多的精力去学习各种细枝末节的功能。并且,Make 也可以通过和第三方编程工具结合,比如 Node、Deno 或者 Python 等脚本编程工具实现各种功能,根本不需要用到 GNU Make 本身提供的 C/C++ 插件机制。

以下为项目目录结构,build-examples 是构建输出目录,stackdump 文件是 GLib 程序运行时错误产生的内存转储文件:


经过以上目录结构设计,就可以很方便地使用 watch 工具进行监测并自动捃重新构建,几乎就是完全自动化的开发构建体验。另外,配合 Sublime Text + Origami 布局插件 + Terminus 控制台插件,使用体验略胜 VS Code(除程序调试功能外)。

项目尝试使用双 Makefile 设计,根目录中的 Makefile 只包含一条 `include src/Makefile` 指令,用于引用 src 目录下的主脚本。主脚本内容及实现的功能如下:



1. 设置默认的构建输出目录 OUTPUT = build-examples

2. 设置默认的源代码目录 SRC = src

3. 设置默认的构建输出的程序文件 BINS,包含 hello.c 和 static.c 两个演示程序;

4. 强制用户在根据目录中执行 make 命令,并将构建生成的文件保存在 OUTPUT 目录;

5. 检测系统是否 Windows NT,如果是就使用 .exe 作为默认程序扩展名;

6. 果系统还没有设置 PKG_CONFIG_SYSROOT_DIR 环境变量,就以 c:/msys64 为默认值;

7. 设置了测试功能,并且为每个程序 touch 一个 .test 目标文件用作更新时间检测;

8. 使用命令前缀 - 忽略错误,可以根据命令返回码来决定是否执行 touch .test;


依赖关系是 all -> prepare BINS test,在构建程序之前先准备创建好输出目录,构建完程序后,再执行测试。测试目标规则中,定义依赖对应的程序目标。每个程序目标规则定义了依赖的源代码文件,由于一个源代码文件生成一个程序,所以这种依赖处理起来显得更容易。另一方面,测试目标依赖关系是:test -> BINS -> sources,所以源代码更新会导致程序重新构建,进行导致测试需要重新执行。如果程序没有更新,就不会触发重新测试。主脚本中主要是使用了 Static Pattern Rules 来重新映射 Targets 或者依赖关系,同时结合高级变量引用表达 `$(VAR:%=%)`,实现目标命名的批量处理、得命名。参考 Make 手册 4.12 Static Pattern Rules 以及 6.3.1 Substitution References。


因为要对待测试程序的返回码乾验证,就需要将测试命令块定义为 `.ONESHELL`,这样才会在一个 shell 进程中执行命令,否则命令按行为单位执行,这就导致无法获取到前面程序的返回码。


最后,使用 watch 工具进行开发测试依然是更方便的操作:


创建 GObject 自定义类型,首先要明确 GLib 各种结构的基本功能与关系,至少要清楚创建一个子类型的基本要素。假定要派生一个 GObject 子类型,即定义一个类型通常需要:


1. 一个包含 `GObject` 的结构体作为类型实例的本体;

2. 一个包含 `GObjectClass` 的结构体作为类型本体;


参考 `g_type_module_get_type` 方法的实现,内部的 `GTypeModuel` 就是这样的一个子类型。GLib 内部使用 `g_type_register_static` 注册的类型,还可以称之为 Module Type。 glib-2.78.0\gobject\gtypemodule.c


可以使用 `GObject` 作为父类型进行扩展,它提供了以下功能:


1. 引用计数器 reference counting,当实例引用计数重置为 0 时就会回收其所占内存;

2. 子类型可以添加任意属性进行扩展;

3. 提供异步信号事件处理服务;


为了简化起见,现在来实现一个不能乾实例化操作的单例静态类型,所谓单例 Singleton 即是指只有一个实例存在的类,这是一种常用编程模式。


最简单的类型定义就是各种内置的值类型,参考宏符号 `G_REAL_CLOSURE`,以及注册各种值类型的 `_g_value_types_init` 方法。


`GTypeInfo` 中的 n_preallocs 字段适用于 GLib 2.10 之前版本,GLib 2.10 开始忽略此字段。用于指定预分配内存空间(预先保留内存空间),0 表示不使用缓存。


如果要定义派生类型,那么派生类型必需使用以上所说的父类型基础结构,在子类型工中称之为重叠结构 boilerplate,因此子类型占据内存空间必然比父类型多。否则,在注册类型时就会给出异常提示信息。如果,后续对错误的类型进行引用、实例化,则会触发运行时错误终止程序:

与 `GObject` 相对应的是 `GObjectClass`,这个结构与 `GObectInfo` 结构同样重要。后者提供类型信息,前者为各种类型提供类型构建、析构、属性访问、消息机制等接口机制。全局类型的定义与注册在 `_g_object_type_init` 方法中完成。


如果是注册顶级基础类型,就像 `GObject` 不需要父类型的信息,使用 `g_type_register_fundamental` 方法进行注册。可以使用 `g_type_fundamental_next()` 为用户定义的顶级类型获取一个自动分配的 ID,


对于可以实例化的类型,那么就可以使用 `g_type_create_instance` 方法进行实例化。如果有成员方法,那么它应该接收一个 instance 作为第一个参数,成员方法的操作应该相对于这个指定的实例对象。就像 Python 中定义类成员方法时,第一个参数总是为 self。


因为类型 ID 已经登记在 GLib 类型节点树中,调用实例化方法时,就会根据指定类型 ID 找到相应的类型定义,并且判断此类型是否是 instantiatable 类型,如果确定是才可以继续进行实例化。


使用 `G_TYPE_CHECK_INSTANCE_TYPE` 或者 `G_TYPE_CHECK_INSTANCE_FUNDAMENTAL_TYPE` 两个宏就可以判断指定的实例与指定类型 ID 是否有关系,从而判断其是否为指定类型的实例。它们根据类型信息结构中提供的 `GTypeInstance` -> `GTypeClass` 关系链来确定其类型:instance->g_class->g_type。如果类型不能通过这个此方法判断,内部还有两个备选的方法,它们会调用 `lookup_type_node_I` 方法去 `TypeNode` 查找节点数据。

以下测试程序应该输出类似以下内容,如果是这样,那么恭喜你已经可以开发 GObject 应用了!


static.c 程序源代码参考如下,注意,以下 KickFundament 和 KickStatic 两个类型共享的同一个 GTypeInfo 信息结构,实际中应该你使用独立的信息结构:


《Makefile 光学教程》之面向 Makefile 编程·C/C++ 项目模板 [Glib-2.0 & ADT]的评论 (共 条)

分享到微博请遵守国家法律