c++:动态库接口函数返回stl对象的设计原则塈‘__acrt_first_block == header’异常原因分析

问题描述

最近在写dll动态库时,动态库函数返回的std::string对象在析构时抛出了异常:
c++:动态库接口函数返回stl对象的设计原则塈‘__acrt_first_block == header’异常原因分析_第1张图片
为简化描述问题,测试代码如下(MSVC /MT 编译),就是返回一个简单的std::string
tools.h

#  if defined(_WIN32) && !defined(__CYGWIN__) 
#    ifdef GFAUX_EXPORTS
#      define GAX_API  __declspec(dllexport)
#    else
#      define GAX_API  __declspec(dllimport)
#    endif
#  else
#    define GAX_API
#  endif
#include 
// 返回一个std::string
GAX_API std::string test();

tools.cpp

#include "tools.h"
std::string test()
{
	return std::string("hello!!!!!");
}

原因分析

关于__acrt_first_block == header异常,google上查了一下,根本的原因是对象在析构时不正确的释放内存导致的。stackoverflow上这篇文章的回复写得比较清晰:Debug Assertion Failed! Expression: __acrt_first_block == header

std::string是STL中定义的模板类,所以编译器在编译动态库时会将std::string实例化,在编译exe时也会将其实例化,也就是说有两套std::string实例代码分别在exe和dll中.
因为我的dll是/MT编译的所以连接的是static crt,所以动态库有自己独立的heap,参见参考资料3.
那么问题来了:
如下面的exe调用代码,当test()返回一个std::string对象给exe时,这个对象的内存是由dll分配的。但在exe中并不能区分这个std::string对象的内存是不是自己的的heap中分配的。在main结束时要析构result,会调用exe中实例化的std::string析构函数代码来释放内存,然后就会抛出__acrt_first_block == header异常。
调用测试代码
main.cpp

#include 
#include "tools.h"
int main(int argc, char *argv[]) {
	std::string result = test();// 从dll返回std::string,result的内存是由dll分配的
	std::cout << result << std::endl;
} // 析构result时抛出异常

如果和exe和动态库都是/MD编译,不会存在上述问题,因为大家使用同一个heap,内存在哪里释放都是一样的。
但我的项目需要必须用静态链接(/MT)所以不能通过修改动态库的编译方式的方法解决问题。

解决方案

知道了原因,就可以推导出解决问题的关键在于不能让exe去析构dll返回的std::string,简单的办法就是在dll中定义一个只包含一个std::string类型成员的class Atest()返回类型改为class A,这样以来exe就不再直接析构std::string,而是析构dll中的class A,class A在析构成员时就能正确释放在当前dll中heap分配的内存了。
如果为每个需要封装的类型都定义一个class A也够烦的,所以可以把这个class A设计成一个模板类raii_dll,它不干别的,只是为了正确释放dll或exe中的对象。代码如下:

	/* 用于dll分配的资源T的raii管理类,析构时自动正确释放资源
	* T为资源类型,外部不可修改
	*/
	template
	class raii_dll {
	public:
		typedef raii_dll _Self;
		typedef T resource_type;
		/* 默认构造函数 */
		raii_dll() :_resource() {}
		/** res 资源对象 */
		explicit raii_dll(const T& res) :
			_resource(res) {
		}
		/* 获取资源引用 */
		const T& get() const noexcept { return _resource; }
		const T& operator*() const noexcept { return get(); }
		const T& operator()() const noexcept { return get(); }
		/* 成员指针引用运算符 */
		const T* operator->()const noexcept { return &get(); }
	private:
		/* 封装的资源对象,外部不可修改 */
		T _resource;
	}; /* raii_dll */

请注意为了确保dll返回的对象不会被赋值为exe的内存对象,这里get()返回的是常量引用(const &)
有了raii_dll这个模板类,我们可以重新设计一下test()的接口定义

tools.h

#  if defined(_WIN32) && !defined(__CYGWIN__) 
#    ifdef GFAUX_EXPORTS
#      define GAX_API  __declspec(dllexport)
#    else
#      define GAX_API  __declspec(dllimport)
#    endif
#  else
#    define GAX_API
#  endif
#include 
#include "raii_dll.h"
// 实例化并导出模板raii_dll,确保只在dll中有一份raii_dll实例代码
template class GAX_API raii_dll;
// 返回raii_dll
GAX_API raii_dll test();

tools.cpp

#include "tools.h"
raii_dll test()
{
	return raii_dll("hello!!!!!");
}

调用测试代码也同步修改
main.cpp

#include 
#include "tools.h"
int main(int argc, char *argv[]) {
	raii_dll result = test();
	// 调用operator()返回对象引用
	std::cout << result() << std::endl;
} 

总结

通过这次跳坑填坑的经历,针对动态的接口设计可以总结几点设计原则,以避免上述的问题,就可以传递复杂类型:

  1. 动态库设计接口时,应该避免直接返回stl类型,如果不可避免(比如本例),就封装将其成一个类返回(可以照搬本文的方法)
  2. 动态库接口函数的输入/出参数如果是class,应尽量设计为常量引用(const &),不允许被修改。
    如本例,如果允许raii_dll中的_resource被exe重新赋值,程序立即就崩了。

参考资料

  1. 《Debug Assertion Failed! Expression: __acrt_first_block == header》
  2. 《跨DLL的内存分配释放问题 Heap corruption》
  3. 《Windows 下主程序与动态库(*.dll)释放对方分配的内存操作要点》

你可能感兴趣的:(动态库,stl,crt,heap,开发工具)