[简介]

一直以来,写C++的一大痛点就是头文件和源文件要同时编写同时管理(参考C++工程基本开发模式.mht),很麻烦,所以白易尝试解决它!

 

MakeBEC 它是:源文件解析自动生成头文件 与 白易模块打包与安装的命令行工具
(参考快速入门编写白易模块.mht中的演示,当中就一直在使用它)

需将白易安装目录设为PATH环境变量后(或通过绿化.bat)便使"MakeBEC"成为全局命令行的功能。

注:Source版配置使用生成前事件,Lib版配置使用生成后事件(原因是需要得到编译成功后的lib文件才能进行打包,而Source版通常需要用作测试故在生成前的事件就能对源代码进行打包,当然还有个很关键因素是MakeBEC还提供了从源文件=>自动解析生成仅含声明的头文件)

白易现已对模块项目集成在初始项目的BEMod_lib.props、BEMod_src.props两个配置里

新建白易模块项目后无需单独配置,开箱即用~

 

[MakeBEC命令行帮助]

参数/特性

功能说明与示例

 默认行为

递归解析当前目录所有 .c .cpp 文件,并在此基础上产生对应的仅含声明的头文件。
之后将这些后缀文件 .cpp .c .hpp .h .lib .pdb .ixx .cppm 打包生成bec压缩包。
如果有 -installG 参数,则还会把其解压到系统模块全局位置。

-excludesrc=

排除需要解析的源文件。支持子目录及通配符(如*.c,甚至*排除所有),多个用空格分割

-excludezip=

排除最后打包进 bec 的文件。支持子目录及通配符(如 *.cpp),多个用空格分割

-outbec=

生成的 bec 模块输出路径。在VS环境中可使用VS环境变量表示对应的工程目录和项目名

-installG

[可选],把生成的模块自动安装到系统模块全局位置。
PS:读取注册表(HKEY_CURRENT_USER\Software\xbcsoft\BE\InstallPath)+"\BEModG"

[场景实例]

source源码发布:

MakeBEC.exe -excludesrc=main.cpp -excludezip=main.cpp Debug Release Source x64 -outbec="$(SolutionDir)/$(SolutionName).bec"

lib静态库发布:

MakeBEC.exe -excludesrc=main.cpp -excludezip=*.cpp *.c *.ixx *.cppm Source x64/Source -outbec="$(SolutionDir)/$(SolutionName).bec"

只要MakeBEC运行后它会对当前目录(GetCurrentDirectory)所有的源文件(-excludesrc之外)进行同目录内生成同名仅含声明的头文件。


 

<MakeBEC的源文件声明提取规则>

只对".cpp .c" 这两种后缀有效,其余后缀不做解析。

 

[常规全局函数]

int add(int a,int b)
{
return a+b;
}
 ↓ 
int add(int a,int b);

 

[带模板的函数]

template<typename T>
auto add(T a, T b)
{
return a+b;
}
 ↓ 
template<typename T>
auto add(T a, T b)
{
return a+b;
}

PS:类中的模板函数,因为它不具备编译单元,且自身就是inline特性故要完整保留。

 

[自动识别类成员函数的定义](注意类前面要带上EXP才可生效)

struct EXP 日期时间型
{
uint 年 = 1970;
char 月 = 1;
char 日 = 1;
char 时区偏移 = 0;
char 时 = 0;
char 分 = 0;
char 秒 = 0;
short 毫秒 = 0;

日期时间型() = default;
日期时间型(const 日期时间型&) = default;
日期时间型& operator=(const 日期时间型&) = default;

日期时间型(int64 时间戳秒, char 跨时区小时 = 期::全局时区){
...
}

int64 取时间戳(可空<char> 跨时区小时 = 空) const;

operator AutoStr(){...}
};


int64 日期时间型::取时间戳(可空<char> 跨时区小时) const
{
struct tm t {};
t.tm_year = 年 - 1900;
t.tm_mon = 月 - 1;
t.tm_mday = 日;
t.tm_hour = 时;
t.tm_min = 分;
t.tm_sec = 秒;
t.tm_isdst = -1;

#ifdef _WIN32
time_t utc = _mkgmtime(&t);
#else
time_t utc = timegm(&t);
#endif

if (utc == (time_t)-1) return 0;
char offset = (跨时区小时 == 空) ? 时区偏移 : (char)跨时区小时;
return (int64)utc - (int64)offset * 3600;
}
 ↓ 
struct 日期时间型
{
uint 年 = 1970;
char 月 = 1;
char 日 = 1;
char 时区偏移 = 0;
char 时 = 0;
char 分 = 0;
char 秒 = 0;
short 毫秒 = 0;

日期时间型() = default;
日期时间型(const 日期时间型&) = default;
日期时间型& operator=(const 日期时间型&) = default;

日期时间型(int64 时间戳秒, char 跨时区小时 = 期::全局时区);

int64 取时间戳(可空<char> 跨时区小时 = 空) const;

operator AutoStr();
};

是的,在外部的成员函数那种会直接去除(但也不是无脑删,会精准识别前缀是类才删,如果前缀被识别为命名空间的函数依旧保留)

但注意:类默认情况下都是保留定义,因为类本身它内部都是inline特性所以没必要去掉定义
而如果看到class或struct EXP 类名则才做去定义的方案,其中EXP为#define EXP __declspec(dllexport)但又要注意这玩意在使用后对于导出整个类其实有副作用,详情:关于导出整个C++类.mht

 

[匿名命名空间namespace{...}]

namespace {
template<class StrT>
size_t _取文本长度(const StrT& 文本数据) { return 文本数据.len(); }
}
size_t 取文本长度W(const StrW& 文本数据) { return _取文本长度(文本数据); }

/**取文本长度
* @param 文本数据
* @return
*/
size_t 取文本长度(const StrA& 文本数据) { return _取文本长度(文本数据); }
 ↓ 
size_t 取文本长度W(const StrW& 文本数据);
size_t 取文本长度(const StrA& 文本数据);

这个用法就很普遍了,先模板内部实现,而后实际生成头文件时去除真实定义。

补充:namespace{...}其C++原本的语义是在一个编译单元中将符号改名为一个随机值,本身可以解决编译单元的静态化,也就是不想对外公开的所有符号你都可以安全在里边定义,并且白易还对此强化了头文件的生成逻辑即让这里边的代码完全去除(支持多处的namespace{...})

 

[关于全局变量]

之前在C++工程基本开发模式.mht#全局变量中有讲述过,全局变量即便你没写"="它也是定义,而要声明全局变量则要在前边加上extern关键字(注意这个不是定义,编译后不产生存储实体),所以白易在生成全局变量的时候头文件会这样处理:

int g_foo = 0; //可以不赋初值
 ↓ 
extern int g_foo;

或者也可以

extern int g_foo = 0; //带extern时必须带上初值(不过我没想到为什么要这样写...)
 ↓ 
extern int g_foo;

若全局变量只在当前一个编译单元中使用正确的做法应当是将其打上匿名命名空间

namespace {
int g_foo = 6; //可赋初值
}

void test()
{
printf("%d", g_foo);
}

当然全局变量的引出也有专属的全局量表来去做,详情:资源表与全局量表.mht

 

不过还有一类非常特殊的全局变量,即希望使用全局变量做main函数之前的初始化情况


如果只是这样确实"_"符号确实可以编译进编译单元,但该'_'符号可能 压根没有被外部引用 所以即便产生编译单元,也没能在实际在链接的阶段被组合进可执行程序中,白易对此的解决方案是加上inline(需配合C++17特性)然后可以完整保留其定义形式(但注意如果该头文件被多处编译单元所包含依旧会执行多次,尽管"_"变量本身是全局唯一一份的,只不过这里SetConsoleOutputCP就算执行多次也没有过多的副作用(目前这样写法只是在于它足够简练),但开发者仍需对此谨慎!)

inline int _ = SetConsoleOutputCP(CP_UTF8);


 ↓ 


 

此外,白易也考虑到了全局变量是类中静态成员的情况

struct MyStruct
{
static int foo;
};
int MyStruct::foo = 0;
 ↓ 
struct MyStruct
{
static int foo;
};

这种在外部的会直接把其定义完全抹去(当然会先精准判断前缀是不是一个类而不是一个命名空间)

不过要注意“类”只能在同个文件中才可被正确识别,如果实在没识别出来而被保留了,则需要下面的忽略区域方案手动移除

[BE_IGNORE区域]

#pragma region BE_IGNORE
// 这里的任何代码在生成头文件时都会被完全抹去,彻底忽略
#pragma endregion

[BE_INLINE区域]

#pragma region BE_INLINE
#include "互斥锁.hpp"
#include "条件锁.hpp"
#include "读写锁.hpp"
#include "自动锁.hpp"
#pragma endregion

在源文件中纯内联式进行包含解析(区域中仅可用#include),最终仅生成一份头文件。

 

[inline+普通函数]

这个可能用的场景比较少,因为我们更多期盼说默认总想着令普通函数直接去除其定义仅保留声明。

总而言之加了inline后它的语义明确对于头文件的生成总是保留其定义,全局变量是如此,函数这里也一样。

不过也是有必需用到的场景,即包含宏的代码时,它务必要在主项目中等主项目开启或关闭该宏后代码才能按照宏的预期逻辑去编译使用,比如下面创建窗口时所给定的默认图标ID的情况

那么该方法我就得做成inline了,让它保留定义

 

[_BETPL_DEF+template函数]

直接先看例子

template<bool 带完整路径, bool 收集> _BETPL_DEF
bool _枚举ImplBase(const StrW& base, bool 是否递归, int 类型,
bool(*回调)(const StrW&), Arraybe<StrW>& 结果) {

WIN32_FIND_DATAW fd = { 0 };
HANDLE h = FindFirstFileW(base + _W("*"), &fd);
if (h == INVALID_HANDLE_VALUE) return true;

bool cont = true;
do {
bool isDir = (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0;
StrW name(fd.cFileName); // 引用构造,零拷贝

if (isDir) {
if (name == _W(".") || name == _W("..")) continue;
if (是否递归) {
cont = _枚举ImplBase<带完整路径, 收集>(base + name + _W("\\"), 是否递归, 类型, 回调, 结果);
if (!cont) break;
}
if (类型 & FILE_ATTRIBUTE_DIRECTORY) {
StrW item;
if constexpr (带完整路径) item = base + name; else item = name;
if constexpr (收集) { 结果.push(item); } else { cont = 回调(item); }
}
} else {
if (类型 & FILE_ATTRIBUTE_NORMAL) {
StrW item;
if constexpr (带完整路径) item = base + name; else item = name;
if constexpr (收集) { 结果.push(item); } else { cont = 回调(item); }
}
}
if (!cont) break;
} while (FindNextFileW(h, &fd));

FindClose(h);
return cont;
}

//白易中使用模板默认会完整保留定义在头文件中生成
//上边模板有再加_BETPL_DEF标记时在白易中会被标记为不保留定义
//而后模板显式实例化产生二进制符号


template bool _枚举ImplBase<false, false>(
const StrW&, bool, int, bool(*)(const StrW&), Arraybe<StrW>&);

template bool _枚举ImplBase<false, true>(
const StrW&, bool, int, bool(*)(const StrW&), Arraybe<StrW>&);

template bool _枚举ImplBase<true, false>(
const StrW&, bool, int, bool(*)(const StrW&), Arraybe<StrW>&);

template bool _枚举ImplBase<true, true>(
const StrW&, bool, int, bool(*)(const StrW&), Arraybe<StrW>&);


template<bool 带完整路径 = false, bool 收集 = false>
auto 文件_枚举(const StrW& 目录, bool 是否递归, 可空<int> 寻找类型 = 空, bool(*回调)(const StrW&) = nullptr) {
int 类型 = 寻找类型.OR(寻找文件::正常文件);
StrW base = 目录;
if (base.len() > 0) { charW c = ((const charW*)base)[base.len()-1]; if (c != L'\\' && c != L'/') base += _W("\\"); }
if constexpr (收集) {
Arraybe<StrW> 结果;
_枚举ImplBase<带完整路径, true>(base, 是否递归, 类型, nullptr, 结果);
return 结果;
} else {
Arraybe<StrW> _; //回调模式不使用,编译器会优化掉
_枚举ImplBase<带完整路径, false>(base, 是否递归, 类型, 回调, _);
}
}


 ↓ 
template<bool 带完整路径, bool 收集>
bool _枚举ImplBase(const StrW& base, bool 是否递归, int 类型,
bool(*回调)(const StrW&), Arraybe<StrW>& 结果);

template bool _枚举ImplBase<false, false>(
const StrW&, bool, int, bool(*)(const StrW&), Arraybe<StrW>&);

template bool _枚举ImplBase<false, true>(
const StrW&, bool, int, bool(*)(const StrW&), Arraybe<StrW>&);

template bool _枚举ImplBase<true, false>(
const StrW&, bool, int, bool(*)(const StrW&), Arraybe<StrW>&);

template bool _枚举ImplBase<true, true>(
const StrW&, bool, int, bool(*)(const StrW&), Arraybe<StrW>&);

template<bool 带完整路径 = false, bool 收集 = false>
auto 文件_枚举(const StrW& 目录, bool 是否递归, 可空<int> 寻找类型 = 空, bool(*回调)(const StrW&) = nullptr) {
int 类型 = 寻找类型.OR(寻找文件::正常文件);
StrW base = 目录;
if (base.len() > 0) { charW c = ((const charW*)base)[base.len()-1]; if (c != L'\\' && c != L'/') base += _W("\\"); }
if constexpr (收集) {
Arraybe<StrW> 结果;
_枚举ImplBase<带完整路径, true>(base, 是否递归, 类型, nullptr, 结果);
return 结果;
} else {
Arraybe<StrW> _; //回调模式不使用,编译器会优化掉
_枚举ImplBase<带完整路径, false>(base, 是否递归, 类型, 回调, _);
}
}

以上这个场景是我在将模块编译到二进制lib所遇到的,希望该模板函数能够有4个实例化,但其真实定义可以不保留到头文件中

由于template函数默认MakeBEC会保留完整定义,在加了_BETPL_DEF标记宏后MakeBEC才会把模板函数的定义去掉;

最后引出的“文件_枚举”,这个就写成模板也没关系因为只是单纯的逻辑转发(真正大头的定义已经不在)。