xmake简介及使用自定义规则处理proto文件

前几天在玩CMake的时候想让CMake来生成pkgconfig的配置文件.pc文件,尝试了很久都没成功,借助DeepSeek也没搞出来。于是研究起了国产构建工具xmake,不用不知道,一用真觉得xmakeCMake好用多了,最主要是的语法简洁,没有CMake怪异语法和晦涩的生成表达式,xmake完全使用的Lua的语言规则。还有一个非常重要的工具就是VSCodexmake插件,可以像CMake插件一样顺畅的工作。

一、xmake简介

1、简单示例

在这里插入图片描述

举一下简单的示例,创建一个xmake工程,xmake create hello:

$ xmake create hello
create hello ...
  [+]: .gitignore
  [+]: src\main.cpp
  [+]: xmake.lua
create ok!

xmake.lua:

add_rules("mode.debug", "mode.release") -- 添加debug和release模式

target("hello") -- 目标为hello
    set_kind("binary") -- 生成可执行文件,如果是static为静态库,shared为动态库
    add_files("src/*.cpp") -- 添加要编译的源文件

日常项目中可能会有多目录的情况,比如一个目录为库,一个目录为依赖这个库的可执行文件:

顶层xmake.lua:

includes("mylib")
includes("app")

mylib的xmake.lua:

target("mylib")
  set_kind("static")
  add_files("*.c")
  add_includedirs(".", {public = true})

app的xmake.lua:

target("mylib")
  set_kind("binary")
  add_files("*.c")
  add_deps("mylib")

感觉入门非常容易。但是要想要在实战项目中得心应手,还需要深入学习很多东西。

2、xmake的目标时机回调函数简介

xmaketarget有一系列的时机回调函数,比如on_XXX,before_XXX,after_XXX类的函数。它们会在不同的时机执行,比如大致有以下一些时机:

  1. 加载目标(on_load)
  2. 配置目标(on_config)
  3. 准备前(before_prepare, before_prepare_file, before_prepare_files)
  4. 准备(on_prepare, on_prepare_file, on_prepare_files)
  5. 准备后(after_prepare, after_prepare_file, after_prepare_files)
  6. 编译前(before_build, before_build_file, before_build_files)
  7. 编译时(on_build, on_build_file, on_build_files)
  8. 编译后(after_build, after_build_file, after_build_files)
  9. 链接前(before_link)
  10. 链接时(on_link)
  11. 链接后(after_link)
  12. 运行前(before_run)
  13. 运行(on_run)
  14. 运行后(after_run)
  15. 清理前(before_clean)
  16. 清理(on_clean)
  17. 清理后(after_clean)
  18. 打包前(before_package)
  19. 打包(on_package)
  20. 打包后(after_package)
  21. 安装前(before_install)
  22. 安装(on_install)
  23. 安装后(after_install)
  24. 卸载前(before_uninstall)
  25. 卸载(on_uninstall)
  26. 卸载后(after_uninstall)

其中以_file结尾的回调函数是处理单个文件的,_files结尾的回调函数是处理多个文件的,从命名上就可以看出来。

3、各阶段的时机顺序

  • 配置期(xmake f)的顺序为1到2
  • 构建期(xmake)的顺序为1到11
  • 运行(xmake r)的顺序为1到14
  • 清理(xmake c)的顺序为配置(1,2)+清理(15,16,17)
    其它阶段类推,打包就是配置+打包,安装就配置+安装,卸载就是配置+卸载。

注意:这个顺序是没有设置预编译头时的一般顺序,如果设置了预编译头,则其优先级会非常高,会在编译前(before_build, before_build_file, before_build_files)编译预编译头。

4、xmake源文件列表的处理

xmake中所有需要编译的源文件列表,一定要在编译(build)之前给出,如果是还没有生成的文件,可以使用always_added来告诉xmake暂时不管文件是否存在,比如描述域可以如下添加:

add_files("$(buildir)/test.pb.c", {always_added = true})

脚本域可以如下添加:

target:add("files", "$(buildir)/test.pb.c", {always_added = true})

如果在编译期的回调函数中添加源文件列表,是不会生效的。

5、自定义规则

如果有多个目标(target)都需要某个功能,比如处理proto文件,就可以使用
自定义规则(rule),让不同的目标(target)使用同样的规则来处理。自定义规则(rule)不仅有目标(target)各时机的所有回调函数,还额外添加了几个函数用于批处理:

  • before_config, after_config
  • before_buildcmd_file,before_buildcmd_files
  • on_buildcmd_file,on_buildcmd_files
  • after_buildcmd_file,after_buildcmd_files

二、使用内置规则"protobuf.c"集成C语言版protobuf

1、 使用官方的protoc-gen-c来生成

如果在项目中使用了protobuf,则可以使用内置的protobuf规则,有“protobuf.c”规则,也有“protobuf.cpp”规则。

app/xmake.lua

add_requires("libprotobuf-c") -- 查找PBC的依赖库
target("app")
	set_kind("binary")
	add_rules("protobuf.c")
	add_files("pb/*.proto", {proto_public = true}) -- 这里需要导出proto 的头文件搜索目录
	add_packages("libprotobuf-c") -- 将PBC的依赖库添加到目标

但是“protobuf.c”是查找的是protoc-c程序,可以查看xmake/rules/protobuf/proto.lua源码:

-- get protoc
function _get_protoc(target, sourcekind)

    -- find protoc
    -- @see https://github.com/xmake-io/xmake/issues/4659
    local envs = os.joinenvs(target:pkgenvs(), os.getenvs())
    local protoc = target:data("protobuf.protoc")
    if not protoc and sourcekind == "cxx" then
        protoc = find_tool("protoc", {envs = envs})
        if protoc and protoc.program then
            target:data_set("protobuf.protoc", protoc.program)
        end
    end

    -- find protoc-c
    local protoc_c = target:data("protobuf.protoc-c")
    if not protoc_c and sourcekind == "cc" then
        protoc_c = find_tool("protoc-c", {envs = envs}) or protoc
        if protoc_c and protoc_c.program then
            target:data_set("protobuf.protoc-c", protoc_c.program)
        end
    end

    -- get protoc
    return assert(target:data(sourcekind == "cxx" and "protobuf.protoc" or "protobuf.protoc-c"), "protoc not found!")
end

在MinGW下会报错:

$ xmake
error: protoc not found!

protoc-c已经被弃用了,官方建议使用protoc程序,它会调用protoc-gen-c来生成C代码。从上面的源码可以看到在会先使用target:data(“protobuf.protoc-c”)获取生成C语言文件的protoc程序,如果没有设置则查找"protoc-c",找到了再使用target:data_set(“protobuf.protoc-c”, protoc_c.program)设置数据。我们可以手动设置这个数据,避免查找"protoc-c"。

app/xmake.lua

add_requires("libprotobuf-c")  -- 查找PBC的依赖库
target("app") -- 目标app
	set_kind("binary") -- 可执行文件
	add_rules("protobuf.c") -- 添加规则
	add_files("pb/*.proto", {proto_public = true})  -- 添加proto文件,这里需要导出proto 的头文件搜索目录
	add_files("*.c") -- 添加C源文件
	add_packages("libprotobuf-c")  -- 将PBC的依赖库添加到目标
	on_load(function (target)
		target:data_set("protobuf.protoc-c","protoc") -- 手动设置PB的C生成器
	end)

这样就可以正常使用了,生成C++代码不用手动设置。xmake会先使用protobuf规则将proto文件生成C/C++源文件和头文件,再编译C/C++源文件。

2、使用自己编译的protoc-gen-c来生成

笔者曾经写过一篇博文:C语言使用Protobuf进行网络通信,里面介绍了如何定义宏来方便快捷的处理Protobuf的网络消息,最终为了方便宏处理,对protobuf-c库进行了一点小调整,也就是需要使用自己修改过源码生成的protoc-gen-c程序。假如自己编译生成的protoc-gen-c程序在app/pb目录中,则需要在处理proto文件前将app/pb目录的路径添加到PATH环境变量中去,才能优先使用自己的程序:

app/xmake.lua

add_requires("libprotobuf-c")  -- 查找PBC的依赖库
target("app") -- 目标app
	set_kind("binary") -- 可执行文件
	add_rules("protobuf.c") -- 添加规则
	add_files("pb/*.proto", {proto_public = true})  -- 添加proto文件,这里需要导出proto 的头文件搜索目录
	add_files("*.c") -- 添加C源文件
	add_packages("libprotobuf-c")  -- 将PBC的依赖库添加到目标
	on_load(function (target)
		target:data_set("protobuf.protoc-c","protoc") -- 手动设置PB的C生成器
		-- 需要使用vformat手动展开变量$(scriptdir),构成绝对路径
		local custom_dir = vformat("$(scriptdir)/pb")
		-- 获取当前 PATH
		local current_path = os.getenv("PATH") or ""
		-- 根据操作系统选择路径分隔符
		local sep = (os.host() == "windows") and ";" or ":"
		-- 将自定义目录添加到 PATH 最前面(确保优先级最高)
		local new_path = custom_dir .. sep .. current_path
		-- 设置PATH变量
		os.setenv("PATH", new_path)
	end)

三、使用自定义规则来集成C语言版protobuf

1、新思路

C语言使用Protobuf进行网络通信中使用自己编译的protoc-gen-c程序来编译,需要自己不断维护源码,比如官方修改了源码,需要自己同步,最近两个版本的protobuf API发生了变化,protoc-gen-c官方都还没同步更新,自己维护的源码编译就会变得困难。其实需求非常简单,就是使用typedef添加一个原类型的别名,比如protoc-gen-c生成的结构为:

typedef struct Netmsg__PingPong Netmsg__PingPong;

我们根据生成的头文件,重新创建一个头文件,添加一个别名:

typedef Netmsg__PingPong netmsg__ping_pong;

只要有了这个别名,就可以使用简化的宏了。在xmake下完全能做到。

2、使用自定义规则处理proto文件

现在理一下有需要自己处理proto文件时,整个目标的构建过程及顺序:

  • 配置目标
  • 处理proto,生成C文件
  • 根据生成的C头文件,按规则创建别名,并生成别名文件
  • 编译链接所有源文件

由于项目可能会使用set_pcheader设置预编译头,预编译头中可能会包含proto生成的头文件,而生成预编译头会在编译所有源文件之前执行,所以所有on_build及后的时机都不行。只有on_build之前的时机可用。

xmake官方建议使用准备阶段的函数,比如:on_prepare(function (target, opt) end),它们是xmake 3.0 新增的函数,专门用于处理各种源码级的预处理、代码生成以及源码依赖分析,后续才会进入 build 阶段。对于有使用set_pcheader设置预编译头的目标,这个时机是非常好的,它在预编译头编译之前执行。

如果使用on_load(function (target)end)或者on_config(function (target)end)来处理,也可以,只是不能像on_prepare(function (target, opt) end)那样有整体的进度信息。但使用编译前(before_build, before_build_file, before_build_files)函数在有设置预编译头时不行,预编译头会先于这些函数执行

下面给出完整的自定义规则代码:

rule("pbc")
	set_extensions(".proto")
	on_prepare_file(function(target, file, opt)
		import("core.project.depend")
		import("utils.progress")
		local rel_dir = path.directory(file) -- app/pb
		local output_dir = path.join(target:autogendir(), rel_dir) -- build/.gens/app/<plat>/<arch>/<mode>/app/pb
		os.mkdir(output_dir) -- 先构建输出目录
		local basename = path.basename(file) -- login
		local prefix = path.join(output_dir,basename) -- -- build/.gens/app/<plat>/<arch>/<mode>/app/pb/login
		local sourcefile = path.join(prefix .. ".pb-c.c") -- build/.gens/app/<plat>/<arch>/<mode>/app/pb/login.pb-c.c
		-- 将生成的源文件添加到需要编译的文件列表
		target:add("files",	sourcefile)
		-- 需要把生成的目录添加进目标的搜索目录,否则会找不到生成的头文件
		target:add("includedirs", output_dir, {public = true})

		-- 检查文件是否存在,如果不存在则需要生成;如果是重编也需要生成
		local need_gen = target:is_rebuilt() or (not os.isfile(sourcefile))
		-- 检查proto文件是否有修改,修改了也需要生成
		depend.on_changed(function ()
			need_gen = true
		end, {files = file})
		if need_gen then -- 需要生成
			progress.show(opt.progress, "compiling.proto.c %s", file)
			os.vrunv("protoc", {"-I".. rel_dir, "--c_out=" .. output_dir, path.filename(file)})
			local headfile = path.join(prefix .. ".pb-c.h")
			local alias_header = path.join(prefix .. ".pb-alias.h")
			local content = io.readfile(headfile)
			local aliases = {}
			-- 驼峰写法变成蛇形写法
			local function camel_to_snake(name)
				name = name:gsub("(%u+)(%u%l)", "%1_%2")
				name = name:gsub("(%l%d)(%u)", "%1_%2")
				name = name:gsub("(%l)(%u)", "%1_%2")
				return name:lower()
			end
			-- 匹配 protobuf-c:
			-- typedef struct Netmsg__PingPong Netmsg__PingPong;
			-- package 名称不固定
			for type_name in content:gmatch("typedef%s+struct%s+([%w_]+__[%w_]+)%s+%1%s*;") do
				local package, message = type_name:match("^(.+)__([%w_]+)$")
				if package and message then
					local alias =	camel_to_snake(package) .. "__" .. camel_to_snake(message) -- 生成别名
					aliases[#aliases + 1] =	string.format("typedef %s %s;", type_name, alias) -- 添加到别名表
				end
			end
			table.sort(aliases) -- 对所有别名排序
			local output = string.format("#pragma once\n\n#include \"%s.pb-c.h\" // IWYU pragma: keep\n\n%s\n",
			 basename, table.concat(aliases, "\n")) -- 生成内容
			io.writefile(alias_header, output) -- 写入文件
		end
	end)

使用很简单:

add_requires("libprotobuf-c")
target("app")
	set_kind("binary")
	add_rules("pbc")
	add_files("pb/*.proto")
	add_files("*.c")
	add_packages("libprotobuf-c")

如本文对你有帮助,欢迎点赞收藏!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值