GNU 开发实用工具:函数、变量与调试技巧
1. 关联数组与命名栈
在开发过程中,关联数组和命名栈是非常实用的数据结构。对于关联数组,可使用
defined
函数来测试键是否存在。
defined
Arguments: 1: Name of associative array
2: The key to test
Returns: $(true) if the key is defined (i.e., not empty)
该函数会返回一个布尔值,指示键是否已定义。
命名栈是一种有序的字符串列表(无空格),在 GMSL 中,栈有内部存储和名称。例如,下面的代码展示了如何使用栈来遍历目录树:
traverse-tree = $(foreach d,$(patsubst %/.,%,$(wildcard $1/*/.)), \
$(call push,dirs,$d)$(call traverse-tree,$d))
$(call traverse-tree,sources)
dump-tree = $(if $(call sne,$(call depth,dirs),0),$(call pop,dirs) \
$(call dump-tree))
$(info $(call dump-tree))
traverse-tree
函数会找到其参数的所有子目录,在深入遍历之前,将找到的目录压入名为
dirs
的栈中。
dump-tree
函数则会从
dirs
栈中弹出元素,直到栈为空。
若要以深度优先的方式遍历目录树,可以使用
dfs
函数:
__dfs = $(if $(call sne,$(call depth,work),0),$(call push,dirs,$(call \
peek,work)$(foreach d,$(patsubst %/.,%,$(wildcard $(call \
pop,work)/*/.)),$(call push,work,$d)))$(call __dfs))
dfs = $(call push,work,$1)$(call __dfs)
$(call dfs,sources)
dump-tree = $(if $(call sne,$(call depth,dirs),0),$(call pop,dirs) $(call \
dump-tree))
$(info $(call dump-tree,dirs))
dfs
函数使用一个名为
work
的工作栈来跟踪要访问的目录,先将起始目录压入
work
栈,然后调用
__dfs
辅助函数。
__dfs
函数将当前目录压入
dirs
栈,将该目录的所有子目录压入
work
栈,然后递归执行,直到
work
栈为空。
2. 栈操作函数
GMSL 提供了一系列栈操作函数,如下表所示:
| 函数名 | 参数 | 返回值 | 功能 |
| ---- | ---- | ---- | ---- |
|
push
| 1: 栈的名称
2: 要压入栈顶的值(不能包含空格) | 无 | 向栈顶添加元素 |
|
pop
| 1: 栈的名称 | 移除栈顶元素后返回该元素 | 检索栈顶元素 |
|
peek
| 1: 栈的名称 | 不移除栈顶元素,返回其值 | 返回栈顶元素的值 |
|
depth
| 1: 栈的名称 | 栈中的元素数量 | 确定栈中元素的数量 |
3. 函数记忆化
为了减少对慢速函数(如
$(shell)
)的调用次数,GMSL 提供了函数记忆化功能。例如,对于计算文件 MD5 值的函数:
md5 = $(shell md5sum $1)
由于
md5sum
执行时间较长,可使用记忆化版本的函数:
md5once = $(call memoize,md5,$1)
这样,对于每个输入的文件名,
md5sum
函数只会调用一次,并将返回值内部记录,后续使用相同文件名调用
md5once
时,无需再次运行
md5sum
即可返回 MD5 值。
4. 杂项与调试工具
GMSL 定义了一些常量,如下表所示:
| 常量 | 值 | 用途 |
| ---- | ---- | ---- |
|
true
|
T
| 布尔值
true
|
|
false
| (空字符串) | 布尔值
false
|
|
gmsl_version
|
1 1 7
| 当前 GMSL 版本号(主版本 次版本 修订号) |
可以像访问普通 GNU make 变量一样,使用
$()
或
${}
来访问这些常量。
此外,GMSL 还提供了
gmsl_compatible
函数来检查当前库版本是否与请求的版本兼容:
gmsl_compatible
Arguments: List containing the desired library version number (major minor
revision)
Returns: $(true) if the current version of the library is compatible
with the requested version number, otherwise $(false)
GMSL 还定义了
gmsl-print-%
目标,可用于打印包含 GMSL 的 makefile 中定义的任何变量的值。例如:
include gmsl
FOO := foo bar baz
all:
gmsl-print-%
使用
make gmsl-print-gmsl_version
可以打印当前 GMSL 版本。
同时,GMSL 提供了两个断言函数
assert
和
assert_exists
:
assert
Arguments: 1: A boolean that must be true or the assertion will fail
2: The message to print with the assertion
Returns: None
assert_exists
Arguments: 1: Name of file that must exist, if it is missing an assertion
will be generated
Returns: None
5. 环境变量
GMSL 有一些环境变量(或命令行覆盖),用于控制各种功能,如下表所示:
| 变量 | 用途 |
| ---- | ---- |
|
GMSL_NO_WARNINGS
| 如果设置,可防止 GMSL 输出警告消息,例如算术函数可能产生的下溢警告 |
|
GMSL_NO_ERRORS
| 如果设置,可防止 GMSL 产生致命错误,如除以零或断言失败 |
|
GMSL_TRACE
| 启用函数跟踪,调用 GMSL 函数时将跟踪函数名和参数 |
这些环境变量可以在环境中设置,也可以在命令行中设置。例如,对于一个包含总是失败断言的 makefile:
include gmsl
$(call assert,$(false),Always fail)
all:
设置
GMSL_NO_ERRORS=1
可以防止断言停止 make 进程,使 make 继续正常运行。
6. 符号与运算符
在开发过程中,还会涉及到各种符号和运算符:
-
\
(反斜杠):作为续行符,用于转义
%
和空格,还可将斜杠转换为反斜杠。
-
:=
运算符:用于变量定义,与
=
运算符有不同的特性,在处理
$(shell)
调用时也有区别。
-
$
:用于变量引用。
-
%
(百分号):作为通配符,需要注意转义。
7. 目录操作
在处理目录时,有许多实用的函数和方法。例如,可以使用
$(wildcard)
函数读取目录条目的缓存:
$(wildcard) to read cache of directory entries, 130–131
还可以使用
$(shell)
调用创建目录:
$(shell) call to create, 133
同时,要检查目录是否存在,可以使用相应的函数。
下面是一个简单的 mermaid 流程图,展示了目录遍历的基本流程:
graph TD
A[开始] --> B[初始化栈]
B --> C[选择起始目录]
C --> D[将起始目录压入工作栈]
D --> E{工作栈是否为空}
E -- 否 --> F[从工作栈弹出目录]
F --> G[将当前目录压入结果栈]
G --> H[查找当前目录的子目录]
H --> I[将子目录压入工作栈]
I --> E
E -- 是 --> J[从结果栈弹出元素并输出]
J --> K{结果栈是否为空}
K -- 否 --> J
K -- 是 --> L[结束]
8. 调试与性能优化
调试 makefile 是开发过程中的重要环节。可以使用 GNU make 调试器进行调试,添加断点函数、设置动态断点等。例如:
GNU make debugger, 58–64
adding breakpoint functions, 67
dynamic breakpoints, 65–69
同时,还可以通过跟踪规则执行和变量值来调试 makefile:
tracing rule execution, 51–55
tracing variable values, 47–51
在性能优化方面,缓存是一个重要的手段。可以缓存变量值,使用
:=
运算符提高速度,还可以使用函数记忆化减少对慢速函数的调用:
caching
speed improvements with, 117–118
variable values, 116–117
function memoization, 220–221
通过这些工具和技巧,可以更高效地进行开发和调试,提高代码的质量和性能。
GNU 开发实用工具:函数、变量与调试技巧
9. 内置函数与用户自定义函数
在开发中,GNU 提供了丰富的内置函数,其调用方式和功能各有特点。内置函数的调用通常需要注意参数的传递,例如使用
$(call)
函数调用内置函数时,要正确处理参数中的空格和逗号。以下是一些常见内置函数及其功能:
| 函数名 | 功能 |
| ---- | ---- |
|
$(abspath)
| 获取文件或目录的绝对路径 |
|
$(addprefix)
| 为列表中的每个元素添加前缀 |
|
$(addsuffix)
| 为列表中的每个元素添加后缀 |
|
$(filter)
| 过滤列表中的元素 |
|
$(filter-out)
| 排除列表中的某些元素 |
同时,用户也可以自定义函数,以满足特定的需求。自定义函数可以使用布尔值进行逻辑操作,还可以实现一些高级功能。例如:
# 自定义函数示例
my_function = $(addprefix prefix-, $1)
在这个示例中,
my_function
函数为输入的参数添加了
prefix-
前缀。
10. 变量操作与管理
变量在 makefile 中起着至关重要的作用,其操作和管理需要谨慎处理。变量的定义方式有多种,如使用
:=
和
=
运算符。
:=
用于定义简单变量,赋值时会立即展开;而
=
用于定义递归展开变量,赋值时不会立即展开。例如:
# 简单变量定义
simple_var := value
# 递归展开变量定义
recursive_var = $(shell command)
变量的作用域分为全局作用域和局部作用域,使用
private
关键字可以定义局部变量。同时,还可以通过
$(eval)
函数和变量缓存来优化变量的使用。例如:
# 变量缓存示例
cached_var := $(call cache, my_function, arg)
在这个示例中,
cached_var
会缓存
my_function
函数对
arg
参数的计算结果,避免重复计算。
11. 依赖管理与规则执行
依赖管理是 makefile 的核心功能之一,它可以确保目标文件在其依赖文件发生变化时及时更新。可以使用自动生成依赖的功能,如
$(wildcard)
函数和
$(patsubst)
函数来生成依赖规则。例如:
# 自动生成依赖规则示例
sources := $(wildcard *.c)
objects := $(patsubst %.c, %.o, $(sources))
all: $(objects)
%.o: %.c
$(CC) -c $< -o $@
在这个示例中,
sources
变量获取所有
.c
文件,
objects
变量根据
sources
生成对应的
.o
文件列表。
%.o: %.c
是一个模式规则,用于编译
.c
文件为
.o
文件。
规则执行的顺序和条件也需要注意,使用
order-only
特性可以定义顺序依赖,确保某些目录或文件在目标执行前已经创建。例如:
graph TD
A[开始] --> B[检查依赖文件]
B --> C{依赖文件是否更新}
C -- 是 --> D[执行规则]
D --> E[更新目标文件]
C -- 否 --> F[跳过规则执行]
E --> G[结束]
F --> G
12. 并行构建与性能优化
并行构建可以显著提高编译速度,使用
-j
或
--jobs
选项可以实现并行执行。但并行构建也有一定的限制,如 Amdahl’s 定律所描述的,部分串行代码会限制并行性能的提升。例如:
# 并行构建示例
make -j4
在这个示例中,
-j4
表示使用 4 个并行任务进行构建。
为了进一步优化性能,还可以使用缓存和函数记忆化等技术。缓存可以减少对慢速函数的调用,函数记忆化可以确保每个输入只计算一次。例如:
# 函数记忆化示例
memoized_function = $(call memoize, slow_function, $1)
在这个示例中,
memoized_function
会记忆
slow_function
对每个输入的计算结果。
13. 字符串与列表操作
在开发中,字符串和列表操作是常见的需求。可以使用
$(subst)
函数替换字符串中的子串,使用
$(sort)
函数对列表进行排序。例如:
# 字符串替换示例
new_string := $(subst old, new, old_string)
# 列表排序示例
sorted_list := $(sort list)
还可以使用
$(foreach)
函数对列表中的每个元素应用一个函数。例如:
# 对列表中的元素应用函数示例
result := $(foreach element, list, $(call my_function, $element))
14. 调试与错误处理
调试 makefile 时,可以使用多种方法。GNU make 调试器提供了丰富的功能,如添加断点函数、设置动态断点等。例如:
# 添加断点函数示例
$(call breakpoint_function, condition, message)
同时,还可以使用
$(warning)
函数输出警告信息,使用
$(error)
函数输出错误信息。例如:
# 输出警告信息示例
$(warning This is a warning message)
# 输出错误信息示例
$(error This is an error message)
在错误处理方面,使用
GMSL_NO_ERRORS
环境变量可以防止断言失败导致 make 进程停止。例如:
# 防止断言失败停止 make 进程示例
make GMSL_NO_ERRORS=1
15. 总结
通过对关联数组、命名栈、栈操作函数、函数记忆化、杂项与调试工具、环境变量、符号与运算符、目录操作、调试与性能优化、内置函数与用户自定义函数、变量操作与管理、依赖管理与规则执行、并行构建与性能优化、字符串与列表操作以及调试与错误处理等方面的介绍,我们了解了 GNU 开发中的各种实用工具和技巧。这些工具和技巧可以帮助开发者更高效地进行开发和调试,提高代码的质量和性能。在实际开发中,需要根据具体需求选择合适的工具和方法,灵活运用这些知识来解决问题。
总之,掌握这些 GNU 开发实用工具和技巧,能够让开发者在开发过程中更加得心应手,应对各种复杂的开发场景。
超级会员免费看
4

被折叠的 条评论
为什么被折叠?



