Skip to content

LV130-自定义函数

一、自定义函数

1. 简介

在 Makefile 中,“自定义函数”本质上是通过 defineendef 关键字创建的 多行变量,它也是命令的集合。可以配合 $(call ...) 函数来调用。它允许你封装复杂的命令或文本处理逻辑,并可以接受参数。

2. 定义函数

和命令包的定义格式相同:

makefile
# 定义一个名为 my_function 的函数,它接受两个参数
define my_function
    @echo "func name is $(0)\n"
    @echo "first param is $(1)\n"
    @echo "second param is $(2)\n"
    @echo "rule's target is: $(@)"
endef

其中 $(0) 是函数名,$(1) 是第 1 个参数,$(2) 是第 2 个参数。$(@) 就是之前学习的自动变量。

3. 调用自定义函数

使用下面的语法格式来调用函数:

makefile
$(call <function_name>, <arg1>, <arg2>, ...)

在函数体内,通过 $(1), $(2), $(3)... 来访问传入的参数。$(@) 代表所有参数。

4. 与命令包的区别

虽然命令包和函数都可以实现代码复用,但它们有重要区别:

  • 函数:返回一个值,用于变量赋值或函数调用
  • 命令包:包含多条命令,用于目标规则中的命令执行

命令包主要用于组织目标规则中的命令,而函数主要用于处理文本或返回值。

5. 使用示例

makefile
define my_function
    @echo "func name is $(0)\n"
    @echo "first param is $(1)\n"
    @echo "second param is $(2)\n"
    @echo "rule's target is: $(@)"
endef

demo:
	# 调用自定义函数,并传递两个参数
	$(call my_function, Hello, World)

我们会得到以下打印信息:

shell
sumu@virtual-machine:~/hk/alpha$ make
# 调用自定义函数,并传递两个参数
func name is my_function\n
first param is  Hello\n
second param is  World\n
all param is: demo

二、函数中的自动变量

$(@), $<, $^ 等是 Makefile 规则的自动变量,在函数体内使用的话依然是自动变量且由当前规则决定。我们直接来看一个示例:

makefile
# 定义一个使用自动化变量的函数
# $@ - 当前规则的目标文件名
# $< - 当前规则的第一个依赖项
# $^ - 当前规则的所有依赖项
define build_rule_info
	@echo "=== 函数打印信息 ==="
	@echo "目标文件: $(@)"
	@echo "第一个依赖: $(<)"
	@echo "所有依赖: $(^)"
	@echo "依赖数量: $(words $(^))"
	@echo ""
endef

# 主要目标,依赖前三个文件
main_target: file1.txt file2.txt file3.txt
	$(call build_rule_info)

# 创建一些测试文件
file1.txt:
	@echo "这是文件1"

file2.txt:
	@echo "这是文件2"

file3.txt:
	@echo "这是文件3"

我们将会看到如下打印信息:

shell
sumu@virtual-machine:~/hk/alpha$ make
这是文件1
这是文件2
这是文件3
=== 函数打印信息 ===
目标文件: main_target
第一个依赖: file1.txt
所有依赖: file1.txt file2.txt file3.txt
依赖数量: 3

三、应用实例

我们参考uboot或者kernel源码中的makefile,我们编写一个生成时间戳头文件的示例。

1. 源码编写

1.1 Kbuild.include

makefile
# filechk_timestamp_autogenerated.h中 只是输出到标准输出,真正的文件写入是通过调用这个函数的规则中的重定向操作符 > 完成的。括号 () 确保所有命令的输出被收集起来,然后一次性重定向到文件。filechk函数中的 > $@.tmp - 这才是写入文件的关键! 它将整个子shell的输出重定向到临时文件

define filechk_timestamp_autogenerated.h
	(\
		LC_ALL=C echo '#ifndef __TIMESTAMP_AUTOGENERATED_H__'; \
		LC_ALL=C echo '#define __TIMESTAMP_AUTOGENERATED_H__'; \
		LC_ALL=C date +'#define DEMO_DATE "%b %d %C%y"'; \
		LC_ALL=C date +'#define DEMO_TIME "%T"'; \
		LC_ALL=C date +'#define DEMO_TZ "%z"'; \
		LC_ALL=C date +'#define DEMO_DMI_DATE "%m/%d/%Y"'; \
		LC_ALL=C date +'#define DEMO_BUILD_DATE 0x%Y%m%d'; \
		LC_ALL=C echo '#endif /* __TIMESTAMP_AUTOGENERATED_H__ */'; \
	)
endef

define filechk
	$(Q)set -e;				\
	: '  CHK     $@';		\
	mkdir -p $(dir $@);			\
	$(filechk_$(1)) < $< > $@.tmp;		\
	if [ -r $@ ] && cmp -s $@ $@.tmp; then	\
		rm -f $@.tmp;			\
	else					\
		: '  UPD     $@';	\
		mv -f $@.tmp $@;		\
	fi
endef

这里的 LC_ALL=C 是国际化设置。强制使用标准 C 语言环境,避免因为系统是中文或法文导致生成的头文件内容不同,从而影响编译哈希值。

外层为什么要有一个括号?

(1)它将所有命令作为一个整体执行:括号创建了一个子shell,里面的所有命令都会在这个子shell中顺序执行。如果去掉括号,每个 echo 和 date 命令都会在独立的进程中执行,这会导致问题。

(2)确保输出顺序正确:在子shell中,所有命令的输出会按顺序收集,然后一次性输出。如果没有括号,多个命令的输出可能会交错或顺序错乱。

1.2 Makefile

makefile
-include Kbuild.include

# 这里可以指定文件目录,例如 a/timestamp_autogenerated.h,则会自动创建a目录
timestamp_h  := timestamp_autogenerated.h
srctree      := .

# Beautify output
# ---------------------------------------------------------------------------
ifeq ("$(origin V)", "command line")
  KBUILD_VERBOSE = $(V)
endif
ifndef KBUILD_VERBOSE
  KBUILD_VERBOSE = 0
endif

ifeq ($(KBUILD_VERBOSE),1)
  quiet =
  Q =
else
  quiet=quiet_
  Q = @
endif

# target
# ---------------------------------------------------------------------------
all: $(timestamp_h)

# 生成时间和版本信息头文件
$(timestamp_h): $(srctree)/Makefile FORCE
	$(call filechk,timestamp_autogenerated.h)

PHONY += clean
clean:
	@rm -rf $(timestamp_h)
# PHONY 伪目标
# ---------------------------------------------------------------------------	
PHONY += FORCE
FORCE:

# Declare the contents of the .PHONY variable as phony.  We keep that
# information in a variable so we can use it in if_changed and friends.
.PHONY: $(PHONY)

2. 运行效果

可以执行下面的命令:

shell
make     # 静默输出
make V=1 # 会显示完整的命令

最后就会发现生成了一个名为 timestamp_autogenerated.h 的文件,里面是生成的时间。