DLL内调用boost::filesystem::path::imbue设置UTF8 locale卸载后致程序崩溃
我们代码库所有字符串统一采用UTF-8编码,为了让boost::filesystem::path能正确将字符串转换为UTF-16格式,需要给它imbue适配的locale。单独向第三方交付供其使用的DLL时这套逻辑运行正常,但如果调用方程序本身也使用了boost::filesystem::path,就会触发异常:
DLL卸载后全局locale似乎被损坏,直接导致程序崩溃。
复现代码
共享库(DLL)代码
#include <boost/filesystem/path.hpp> #include <codecvt> #include <iostream> extern "C" __declspec(dllexport) void thefunction() { boost::filesystem::path::imbue(std::locale(std::locale(), new std::codecvt_utf8_utf16<wchar_t>())); boost::filesystem::path path = "a path in the dll"; std::wcout << path.wstring() << "\n"; }
可执行程序(调用方)代码
#include <boost/filesystem/path.hpp> #include <iostream> #include <Windows.h> int wmain(int argc, wchar_t* argv[]) { auto handle = LoadLibraryEx(L"thedll.dll", NULL, LOAD_WITH_ALTERED_SEARCH_PATH); auto thefunction = (void(*)())GetProcAddress((HMODULE)handle, "thefunction"); thefunction(); FreeLibrary(handle); boost::filesystem::path path = "a path in the exe"; // <--- 此处触发崩溃! std::cout << path.string() << "\n"; }
核心疑问
是否存在DLL内imbue locale的正确实现方式,能够保证DLL卸载后相关全局状态仍然合法有效?
补充说明
- 测试环境:Boost 1.55、MSVC2010
- 生产代码中imbue调用位于
DllMain的DLL_PROCESS_ATTACH阶段 - 已经尝试过在
DLL_PROCESS_DETACH阶段通过预先存储的全局变量恢复原有locale,但该方案在级联DLL加载场景(所有关联DLL都使用相同的locale设置机制)下会引发状态冲突 - 代码中
LOAD_WITH_ALTERED_SEARCH_PATH为必填参数,无法移除
崩溃的核心原因是传入imbue的locale对象持有的codecvt_utf8_utf16 facet是由当前DLL分配的,当DLL被FreeLibrary卸载后,该facet的代码地址、虚表指针全部失效。而boost::filesystem::path的全局locale是进程级全局状态,不会随DLL卸载自动重置,后续EXE或者其他未卸载模块调用path的编码转换逻辑时,就会跳转到已经被卸载的内存地址执行代码,直接触发访问违例崩溃。
之前尝试的DETACH阶段恢复locale的方案在级联DLL场景失效也符合预期:多个DLL按顺序加载时都会覆盖全局locale,卸载顺序如果和加载顺序不一致,先卸载的DLL把全局locale恢复成自己加载前存的旧值,就会把后加载DLL设置的有效locale覆盖掉,后续其他模块使用时要么拿到无效facet,要么拿到不符合预期的编码转换规则。
方案1:不修改进程全局path locale,DLL内部完成路径编码转换(最稳妥,零全局状态污染)
这是交付第三方DLL的最优方案,完全不触碰全局状态,从根源避免跨模块状态冲突:
- 不要调用全局的
boost::filesystem::path::imbue - DLL内部所有需要构造path的场景,不要直接把UTF-8字符串传给path构造函数,手动用DLL内部持有的、生命周期和DLL绑定的codecvt做编码转换,转成
wchar_t宽字符串后再构造path:
// DLL内部全局持有,生命周期和DLL一致,不会在DLL卸载前失效 inline boost::filesystem::path dll_make_path(const std::string& utf8_str) { std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>> conv; return boost::filesystem::path(conv.from_bytes(utf8_str)); }
这套逻辑完全不修改任何全局状态,不管DLL加载卸载顺序如何、不管其他模块怎么设置path的locale,都不会互相影响,也不存在facet悬空的问题。
方案2:如果必须使用全局imbue,严格保证facet生命周期覆盖整个进程运行周期
如果业务逻辑实在绕不开全局imbue(比如大量存量第三方代码直接依赖默认path构造的编码行为,改造成本过高),就必须保证传入的codecvt facet在进程退出前绝对不会被卸载:
- 不要把imbue逻辑放在DllMain里,DllMain运行时处于加载器锁阶段,涉及locale、CRT、Boost的操作很容易引发死锁或者未定义行为
- 不要在普通DLL里定义facet的实现,把codecvt facet、locale初始化逻辑放在一个永远不会被卸载的公共基础模块中;如果做不到公共模块拆分,就实现跨DLL的引用计数机制:每个设置UTF-8 locale的DLL加载时给全局引用计数+1,卸载时只有引用计数归0,才能把全局locale恢复为系统默认值,且恢复操作要确保没有其他模块还在使用旧的facet。
额外优化建议
当前使用的Boost 1.55版本的boost::filesystem::path本身就存在跨模块locale状态管理的设计缺陷,高版本Boost已经逐步改为默认支持UTF-8路径转换,不需要手动imbue,如果条件允许升级Boost版本,能从根本上规避这类跨DLL的locale冲突问题。
内容的提问来源于stack exchange,提问作者keinkoenig

