前言
在前面,我曾经写过一篇文章 《字符串格式化漫谈》 文章最后提到了 Bela 里面实现了一个类型安全的 bela::StrFormat,实际上 bela 还有很多有趣的功能,本文也就是说一说 Bela 有哪些有趣功能和故事。
一. Bela 的缘由
之前,我开发了几个开源软件,如 Windows 系统以指定权限启动进程的 Privexec,Clang Windows 操作系统上自动化构建依赖管理工具 Clangbuilder,还有 PE 分析工具 PEAnalyzer,文件分析工具 Planck 等等。在编写这些工具时要重复编写一些代码,毕竟大家都知道 C++ STL 有时候并不能称心如意。在 Google Abseil 开源后,我借鉴了这个项目的一些代码重新造了一些 wchar_t 版本的轮子,后来把这些代码单独抽离出来,进一步改进,也就成了现在的 Bela。不直接用 Absl 的原因很简单,它不支持 wchar_t。格式化字符串不使用 fmtlib 的原因也很简单,不喜欢异常,它的代码库也比较大。叫 bela ['bələ] 的原因依然很简单,简短易读易拼写。
Bela 的字符串函数基本基于 Abseil,Unicode 转换基于 LLVM 的 ConvertUTF.cpp,最初 ConvertUTF 的版权属于 Unicode.org , charconv 基于 Visual C++ STL,EscapeArgv 借鉴了 Golang 源码,endian.hpp,tokenziecmdline.hpp 借鉴了 LLVM Support Library 等等。
二. Bela 字符串功能库
bela::error_code
bela::error_code 位于 <bela/base.hpp> 类似 std::error_code,其主要目的是简化 Windows API 错误信息的格式化,当人们使用 make_system_error_code 就可以将 Windows 错误信息保存到 bela::error_code 对象,利用 C++ RAII 机制,完全可以不用担心内存释放,同样你还可以使用 bela::make_error_code 构造自己的错误信息,bela::make_error_code 依赖 bela::strings_internal::CatPieces,因此,你可以像使用 bela::StringCat 使用 bela::make_error_code
StringCat
StringCat 学习了 absl::StrCat,唯一的不同在于使用 wchar_t 作为字符基本类型,在 Windows 系统中,StrCat 实际上被 shlwapi 作为宏定义使用了,为了避免在使用 Windows API 时造成困惑,我将其命名为 StringCat。这种函数的好处是连接字符串时只需要一次内存分配,可以将基本类型转变为字符串类型,然后连接到一起,十分有用,并且,bela::StringCat 比 absl::StrCat 有趣的一点是支持 char32_t Unicode 码点,因此,你可以使用 bela::StringCat 连接 Unicode 码点来拼接 Emoji 或者其他字符,然后输出到 Windows Terminal 或者显示到文本或者使用 Dwrite 绘制到图形界面上。
StringCat 定义在 <bela/strcat.hpp> 文件中。
Ascii
bela 学习了 absl/strings/ascii.h ,并将其移植到 wchar_t。在移植的时候要考虑在 Windows 系统上 wchar_t 的范围是 0~65535,因此一些函数需要判断大于 0xFF 时的策略。在 <bela/ascii.hpp> 文件中 AsciiStrToUpper ,AsciiStrToLower, StripLeadingAsciiWhitespace,StripTrailingAsciiWhitespace, StripAsciiWhitespace 这些函数十分有帮助。
其他字符串函数
| 函数 | 功能 | 文件 |
|---|---|---|
| StrSplit | 字符串切割,Delimiter 支持按字符,按字符串或者按长度,支持跳过空或者空白,比 Golang 的 strings.Split 要好用一些 | <bela/str_split.hpp> |
| StrReplaceAll | 替换字符串 | <bela/str_replace.hpp> |
| ConsumePrefix,ConsumeSuffix,StripPrefix,StripSuffix | 删除特定前缀或者后缀 | <bela/strip.hpp> |
| StartsWith,EndsWith,EqualsIgnoreCase,StartsWithIgnoreCase,EndsWithIgnoreCase | 特定的比较函数,前两者 C++20 被引入(std::string::starts_with,std::string::ends_with),Visual 2019 16.1 C++ /std:c++latest 开启 | <bela/match.hpp> |
| Substitute,SubstituteAndAppend | 在前文 《字符串格式化漫谈》 有提及,字符串填充。 | <bela/subsitute.hpp> |
| SimpleAtob,SimpleAtoi | 字符串转整型或者 Boolean,要转换浮点类型,请使用 <bela/charconv.hpp> | <bela/numbers.hpp> |
编码转换
在 Bela 中,我基于 LLVM 的 ConvertUTF 实现了 UTF-16/UTF-8 UTF-32 的一些函数,声明文件均在 <bela/codecvt.hpp>
| 函数 | 功能 |
|---|---|
| char32tochar16 | Unicode 码点转 UTF-16,缓冲区长度应当至少为 2 |
| char32tochar8 | Unicode 码点转 UTF-8,缓冲区长度至少为 4 |
| c16tomb | UTF16 编码转 UTF-8,低级 API |
| mbrtowc | UTF-8 编码转 UTF-16 (wchar_t),低级 API |
| mbrtoc16 | UTF-8 编码转 UTF-16 (char16_t),低级 API |
| ToNarrow | UTF-16 转 UTF-8 |
| ToWide | UTF-8 转 UTF-16 |
涉及到编码转换时,应当使用高级 API bela::ToNarrow 和 bela::ToWide
StrFormat
Bela 目前提供了一个类型安全简单的 StrFormat, StrFormat 基于 C++ 变参模板,使用 union 记录参数类型,在解析时按照输入的占位符将其他类型转换为字符串连接在一起,从而实现格式化功能。bela::StrFormat 借鉴了 Chromium SafeNPrintf 函数,但支持的类型要比 SafeNPrintf 多很多。
支持的类型和响应的占位符如下表所示:
| 类型 | 占位符 | 备注 |
|---|---|---|
| char | %c | ASCII 字符,会被提升为 wchar_t |
| unsigned char | %c | ASCII 字符,会被提升为 wchar_t |
| wchar_t | %c | UTF-16 字符 |
| char16_t | %c | UTF-16 字符 |
| char32_t | %c | UTF-32 Unicode 字符,会被转为 UTF-16 字符,这意味着可以使用 Unicode 码点以 %c 的方式输出 emoji。 |
| short | %d | 16位整型 |
| unsigned short | %d | 16位无符号整型 |
| int | %d | 32位整型 |
| unsigned int | %d | 32位无符号整型 |
| long | %d | 32位整型 |
| unsigned long | %d | 32位无符号整型 |
| long long | %d | 64位整型 |
| unsigned long long | %d | 64位无符号整型 |
| float | %f | 会被提升为 double |
| double | %f | 64位浮点 |
| const char * | %s | UTF-8 字符串,会被转换成 UTF-16 字符串 |
| char * | %s | UTF-8 字符串,会被转换成 UTF-16 字符串 |
| std::string | %s | UTF-8 字符串,会被转换成 UTF-16 字符串 |
| std::string_view | %s | UTF-8 字符串,会被转换成 UTF-16 字符串 |
| const wchar_t * | %s | UTF-16 字符串 |
| wchar_t * | %s | UTF-16 字符串 |
| std::wstring | %s | UTF-16 字符串 |
| std::wstring_view | %s | UTF-16 字符串 |
| const char16_t * | %s | UTF-16 字符串 |
| char16_t * | %s | UTF-16 字符串 |
| std::u16string | %s | UTF-16 字符串 |
| std::u16string_view | %s | UTF-16 字符串 |
| void * | %p | 指针类型,会格式化成 0xffff00000 这样的字符串 |
如果不格式化 UTF-8 字符串,且拥有固定大小内存缓冲区,可以使用 StrFormat 的如下重载,此重载可以轻松的移植到 POSIX 系统并支持异步信号安全:
template <typename... Args>
ssize_t StrFormat(wchar_t *buf, size_t N, const wchar_t *fmt, Args... args)
我们基于 StrFormat 实现了类型安全的 bela::FPrintF,这个函数能够根据输出设备的类型自动转换编码,如果是 Conhost 则会输出 UTF-16,否则则输出 UTF-8。如果 Conhost 不支持 VT 模式,bela 则会将输出字符串中的 ASCII 颜色转义去除,但 bela 并没有做 Windows 旧版本的适配,我们应该始终使用 Windows 最新发行版。
下面是一个示例:
/// C++17
#include <bela/strcat.hpp>
#include <bela/stdwriter.hpp>
constexpr auto cv=__cplusplus;
int wmain(int argc, wchar_t **argv) {
auto ux = "\xf0\x9f\x98\x81 UTF-8 text \xE3\x8D\xA4"; // force encode UTF-8
wchar_t wx[] = L"Engine \xD83D\xDEE0 中国";
bela::FPrintF(
stderr,
L"Argc: %d Arg0: \x1b[32m%s\x1b[0m W: %s UTF-8: %s C++ version: %d\n", argc, argv[0], wx, ux, cv);
char32_t em = 0x1F603;//😃
auto s = bela::StringCat(L"Look emoji -->", em, L" U: ",
static_cast<uint32_t>(em));
bela::FPrintF(stderr, L"emoji test %c %s\n", em, s);
bela::FPrintF(stderr, L"hStderr Mode: %s hStdin Mode: %s\n",
bela::FileTypeName(stderr), bela::FileTypeName(stdin));
return 0;
}
请注意,如果上述 emoji 要正常显示,应当使用 Windows Terminal 或者是 Mintty。
三. Bela Windows 系统功能库
bela::finaly
在使用 Golang 时,defer 可以在函数退出时执行一些代码,在 C++ 中 Microsoft/GSL 里面有一个 finaly 实现,异曲同工。在这里 我们可以使用 finaly 避免资源泄露。
#include <bela/finaly.hpp>
#include <cstdio>
int wmain(){
auto file=fopen("somelog","w+");
auto closer=bela::finaly([&]{
if(file!=nullptr){
fclose(file);
}
});
/// do some codes
return 0;
}
PathCat 路径规范化连接函数
PathCat 函数借鉴了 StringCat 函数,将路径组件连接起来。例子如下:
auto p = bela::PathCat(L"\\\\?\\C:\\Windows/System32", L"drivers/etc", L"hosts");
bela::FPrintF(stderr, L"PathCat: %s\n", p);
auto p2 = bela::PathCat(L"C:\\Windows/System32", L"drivers/../..");
bela::FPrintF(stderr, L"PathCat: %s\n", p2);
auto p3 = bela::PathCat(L"Windows/System32", L"drivers/./././.\\.\\etc");
bela::FPrintF(stderr, L"PathCat: %s\n", p3);
auto p4 = bela::PathCat(L".", L"test/pathcat/./pathcat_test.exe");
bela::FPrintF(stderr, L"PathCat: %s\n", p4);
PathCat 的思路是先将 UNC 前缀和盘符记录并去除,然后将所有的参数使用 PathSpilt 函数以 Windows 路径分隔符和 Linux 路径分隔符拆分成 std::wstring_view 数组,当当前路径元素为 .. 时,弹出字符串数组一个元素,如果为 . 则保持不变,否则将路径元素压入数组。拆分完毕后,遍历数组计算所需缓冲区大小,调整 std::wstring 容量,然后进行路径重组。
当第一个参数值为 . 时,PathCat 将解析第一个路径为当前目录,然后参与解析。如果 PathCat 第一个参数是相对路径,PathCat 并不会主动将路径转变为绝对路径,因此,你应当主动的将第一个参数设置为 . 以期解析为绝对路径。
PathCat 并不会判断路径是否存在,因此需要注意。
路径解析错误是很多软件的漏洞根源,合理的规范化路径非常有必要,而 PathCat 在规范化路径时,使用 C++17/C++20(Span) 的特性,减少内存分配,简化了规范化流程。
PathCat 使用了 bela::Span (<bela/span.hpp>),Span 被 C++20 采纳,bela::Span 基于 absl::Span。
PathExists 函数
PathExists 函数判断路径是否存在,当使用默认参数时,只会判断路径是否存在,如果需要判断路径的其他属性,可以使用如下方式:
if(!bela::PathExists(L"C:\\Windows",FileAttribute::Dir)){
bela::FPrintF(stderr,L"C:\\Windows not dir or not exists\n");
}
LookupRealPath 函数
LookupRealPath 用于解析 Windows 符号链接和卷挂载点。
LookupAppExecLinkTarget 函数
LookupAppExecLinkTarget 用于解析 Windows AppExecLink 目标,在 Windows 10 系统中,AppExecLink 是一种 Store App 的命令行入口,通常位于 C:\Users\$Username\AppData\Local\Microsoft\WindowsApps,这种文件本质上是一种重解析点,因此解析时需要按照重解析点的方法去解析。
ExecutableExistsInPath 查找可执行文件
在 Windows cmd 中,有一个命令叫做 where,用于查找命令或者可执行文件的路径,而 ExecutableExistsInPath 则提供了相同的功能,我们可以按照输入的命令或者路径查找对应的可执行文件,这里有一个 where 实现:
////
#include <bela/strcat.hpp>
#include <bela/stdwriter.hpp>
#include <bela/path.hpp>
int wmain(int argc, wchar_t **argv) {
if (argc < 2) {
bela::FPrintF(stderr, L"usage: %s command\n", argv[0]);
return 1;
}
std::wstring exe;
if (!bela::ExecutableExistsInPath(argv[1], exe)) {
bela::FPrintF(stderr, L"command not found: %s\n", argv[1]);
return 1;
}
bela::FPrintF(stdout, L"%s\n", exe);
return 0;
}
命令行合成,拆分和解析
在 bela 中,我们提供了命令行合成,拆分和解析类,具体如下:
| 类名 | 功能 | 文件 |
|---|---|---|
| ParseArgv | 解析命令行参数,类似 GNU getopt_long,支持 wchar_t,不使用全局变量,错误信息详细 | <bela/parseargv.hpp> |
| Tokenizer | 将命令行字符串 Windows commandline 形式转变为 wchar_t **Argv 形式 | <bela/tokenizecmdline.hpp> |
| EscapeArgv | 将 Argv 形式命令行参数转为 commdline 形式,主要用于 CreateProcess | <bela/escapeargv.hpp> |
MapView
在 bela 中,我还提供 MapView,这是一个只读的文件内存映射,通常用于文件解析。文件 <bela/mapview.hpp> 还有与 std::string_view 类似的 MemView 类。
PESimpleDetailsAze 获得 PE 的简单信息
在 Bela 中,我添加了一个 PESimpleDetailsAze 用于获得 PE 可执行文件的一些信息,其结构体如下:
struct PESimpleDetails {
std::wstring clrmsg;
std::vector<std::wstring> depends; // depends dll
std::vector<std::wstring> delays; // delay load library
PEVersionPair osver;
PEVersionPair linkver;
PEVersionPair imagever;
Machine machine;
Subsytem subsystem;
uint16_t characteristics{0};
uint16_t dllcharacteristics{0};
bool IsConsole() const { return subsystem == Subsytem::CUI; }
bool IsDLL() const {
constexpr uint16_t imagefiledll = 0x2000;
return (characteristics & imagefiledll) != 0;
}
};
函数的声明如下:
std::optional<PESimpleDetails> PESimpleDetailsAze(std::wstring_view file,
bela::error_code &ec);
通过此函数,你可以获得 PE 可执行文件的目标机器类型,子系统,连接器版本,系统版本,Image 版本,PE 的特征,PE 依赖的 dll 和延时加载的 dll。如果是 CLR PE 文件,则clrmsg 不为空描述的是 CLR 的信息。PESimpleDetailsAze 并不依赖 DbgHelp.dll (ImageRvaToVa)。
最后
Bela 应该是不断发展的,如果我有新的 Idea 了,就会及时的移植进去的。
Last modified on 2019-05-25