背景
Parser Generator 2 最糟糕的设计,莫过于要使用LibBuilder来构建C/C++库文件。
下载后其默认的配置是为Visual Studio 6.0(以及Borland C)配置的,是32位。但后来Visual Studio(VS)的发展速度很快,C/C++用户使用Parser Generator 2遇到的第一个问题就是为最新的VS做配置。但往往最新的VS会改变编译参数和编译配置,另外我们还有可能要编译64位版本,这就需要用户修改相关的脚本。这个脚本在Cpp\Scripts下,其内容看起来是一种脚本语言。那就意味着开发这还需搞懂这种脚本语言。在这一步很多开发者就放弃了,非常可惜。
代码分析
今天本文要脱离LibBuilder,直接编译库文件。通过观察,我们发现所有的C/C++源代码来自Cpp\Source目录下。那么这就意味着我们可以直接从这些源代码入手进行编译。
首先看Cpp\Lib\msvc32下面的库文件。这里有一篇文章:
https://blog.csdn.net/xujianlane/article/details/2801204
看里面列的两个表格,基本上说清楚了。yl.lib是只支持单线程静态库,ylmt.lib是支持多线程静态库,ylmtr.lib是支持多线程run time类的静态库,ylmtri.dll是支持多线程run time类的动态库。
由于最初发布该工具的时候,PC机的处理能力并没有现在那么强劲,运行效率非常重要,所以设计者提供了多种模式的库来适应用户的性能要求,显然静态单线程的库会比动态多线程的类库快很多。但是从今天计算机处理能力来看,这种差别已经可以忽略了。使用类开发已经是主流,所以我们今天的目标只要编出ylmtri.dll即可。如果有特殊需求,需要在性能低的设备上运行,可以根据本文的内容类推。
在Cpp\Source下,有300多个文件。但是仔细看前缀,是非常有规律的。忽略开头的yy,可以看到有ac, am, as, wc, wm, ws,六种。
如果你选择查看Visual C++ (32-bit)编译配置的属性页,其中有一个wchar_t的支持。
所以可以猜到 a 就是支持ascii, w就是支持wchar_t。从编译出库的模式看,s就是single,单线程;m就是multiple,多线程;c就是class,即支持类。
a | 只支持8bit字符 |
w | 支持宽字符 |
s | 只支持单线程,不支持类 |
m | 支持多线程,不支持类 |
c | 支持多线程,类 |
从这里可以看到,a和m的选择其实是在用LibBuilder编类库的时候确定下来的。如果Treat wchar_t选项是True,那么yya*那些源代码根本不会被编译。显然我们只需要编yyw*的代码。另外既然我们的目标是使用类的版本,那么我们需要编译的代码就缩小到wc的代码。即只需要编译所有代码的1/6,大概是50多个文件就够了。
另外我们还看到一些不带特殊前缀的文件:
我们打开yydllm.c看一下:
#ifdef YYPROTOTYPE
BOOL WINAPI DllMain(HANDLE hDLL, DWORD dwReason, LPVOID lpReserved)
#else
BOOL WINAPI DllMain(hDLL, dwReason, lpReserved)
HANDLE hDLL;
DWORD dwReason;
LPVOID lpReserved;
#endif
#endif
{
switch (dwReason) {
case DLL_PROCESS_ATTACH:
yyinit();
break;
case DLL_PROCESS_DETACH:
yydelete();
break;
case DLL_THREAD_ATTACH:
break;
case DLL_THREAD_DETACH:
break;
default:
break;
}
return TRUE;
}
#endif
哦,原来是dll入口函数,yyinit()不就是yyinit.c提供的,yydelete()不就是yydelete.c提供的。看来这几个文件也要编进去。
编译
现在我们编译的目标如下:
支持多线程
支持宽字节
支持Unicode
使用类
生成动态链接库
我们使用最新版本的Visual Sutidio 2022 Community来做这个事情。
环境脚本
我们创建一个目录PG2Libs作为我们的工作目录(<work_dir>)。在这个目录下创建一个批处理文件env.bat(我的系列都用这个模式),内容如下:
set PGDIR=D:\Program Files (x86)\Parser Generator 2
set VSINSTALLDIR=D:\Program Files\Microsoft Visual Studio\2022\Community
set PATH=%VSINSTALLDIR%\Common7\IDE;%PGDIR%\Bin;%PATH%
start cmd /k devenv
前面的set是用来设置环境的,包括路径和变量。第一行是设置PGDIR为Parser Generator 2的安装目录。第二行是设置VS的安装目录。第三行是将相关的可执行文件位置加入到PATH里。读者可以根据自己安装的目录来写。
这样最后一行启动的VS,并使用上面设置的环境。
创建项目
我们还是使用ylmtri来命名项目,解决方案的名字写GP2Libs。这样还可以在这个解决方案里加别的项目来编别的库:
注意,位置要填写GP2Libs所在的目录
选择空项目:
在属性页里,配置类型选动态库。注意,调试版在目标文件名后加一个d
Include目录配置:
这里使用了%PGDIR%,这个就是env.bat里定义的
预处理宏:
预处理宏可以查看Cpp\Source\yytdefs.h的内容,通过总结:
Debug版本 | Release版本 |
_WIN32 _DLL _MT YYDEBUG YYBUILDDLL YYNUNICODE | _WIN32 _DLL _MT YYBUILDDLL YYNUNICODE |
接下来就可以添加代码了,添加所有的yywc开头的文件,另外添加不带特殊前缀的c文件就可以了。
另外创建完解决方案,读者可以修改env.bat最后一行:
start cmd /k devenv PG2Libs.sln
这样运行这个批处理就可以直接打开解决方案。
编译项目
上面的配置都好了以后,就可以开始编译了。编译过程主要是处理出错.
错误
PG2Libs\ylmtri\yywcdop.cpp(37,3): error C4996: 'wcscpy': This function or variable may be unsafe. Consider using wcscpy_s instead. To disable deprecation, use _CRT_SECURE_NO_WARNINGS. See online help for details.
这错误是说wscpy已经不安全了,建议使用wscpy_s或者定义_CRT_SECURE_NO_WARNINGS关闭这个错误。
解决一
如果你选择改wscpy_s,那么调用时要增加一个参数,就是目标字符串的可用长度。比如:
void yywlexer::yydebugoutput(yywint_t ch) const
{
wchar_t string[32];
switch (ch) {
case EOF:
// wcscpy(string, L"EOF"); //原来的写法
wcscpy_s(string, 32, L"EOF"); //修改后的写法
break;
...
同样的方法来修改swprintf:
// output stack contents
if (yygetglobaldebugstack() || yydebugstack) {
yydebugoutput(L"\nstack");
int n = swprintf_s(string, 128, L"\n +"); // 修改后
int i;
for (i = 0; i < 10; i++) {
n += swprintf_s(&string[n], 128 - n, L" %5d", (int)i); //修改后
}
yydebugoutput(string);
...
注意,向string[n]拷贝,长度要去掉n。
解决二
在yydefs.h文件底部添加宏定义:
#define _CRT_SECURE_NO_WARNINGS
比较建议解决方案一,既然已经使用了最新的编译工具,就彻底排除隐患。
通过修改就可以顺利编译了,在Debug配置下最终得到ylmtrid.dll
如此编译Relase版和64位版本都不是问题了。
总结
本文总结了不使用LibBuilder的方法来构建Parser Generator 2的类库。站在现在的开发工具角度看,编译多线程支持类的动态库是最好的选择,在这一选择下通过分析源代码的类型,构建该库并不困难。当彻底摆脱了LibBuilder的限制,在任何VS版本下使用Parser Generator 2都不是问题了。