在.NET 4中调用GDAL库时遇到的问题及解决方法
最近需要在.NET 4的环境中调用GDAL库。GDAL本身是一套非托管类库,不过还好提供了用SWIG做的托管的Wrapper。
可以在FWTools的安装包中找到这些Wrapper的编译好的dll文件,不过FWTools中附带的版本依赖于gdal_fw.dll(gdal_fw.dll是GDAL核心类库的修改版),而gdal_fw.dll依赖的其他非托管程序集太多了,加起来有18M左右。所以还是自己下载代码编译的好。
这篇文章介绍了1.4版本的下载和编译方法,该方法同样适用于现在最新的1.7版本。
编译好之后引用、调用、Debug都没问题,一切正常,但是如果用Release编译并在VS之外运行的话则会报出AccessViolationException,异常信息提示说访问了受保护的内存。我的第一反应就是托管的Wrapper中用P/Invoke调用了非托管程序集,而非托管程序集导致了这个问题。但是这个猜测并不能解释为什么只有在.NET 4+Release+IDE外运行的情况下才会出错的现象。
猜来猜去,找来找去找到了问题的所在:
GDAL的托管Wrapper中有一个叫做SWIGStringHelper的类型,该类型的静态构造方法中执行了一些比较重要的初始化操作。另外一个叫做OsrPINVOKE的类中声明了一个SWIGStringHelper类型的私有静态字段,并在声明时就new了该字段,而且OsrPINVOKE中没有显式声明的静态构造。
把这两个类型的代码简化一下的话,大概是这样的:
class OsrPINVOKE
{
private static SWIGStringHelper helper = newSWIGStringHelper();
public static void DoSomething()
{
Console.WriteLine("static method of OsrPINVOKE");
}
}
class SWIGStringHelper
{
static SWIGStringHelper()
{
//这里做了一些重要的初始化
Console.WriteLine("SWIGStringHelper static constructor");
}
}
如果有代码调用DoSomething,这段代码执行顺序估计是这样的:
OsrPINVOKE的静态构造方法(里面初始化helper这个静态字段);
SWIGStringHelper的静态构造方法(输出字符串);
SWIGStringHelper的实例构造方法(里面啥也没有做);
DoSomething方法(输出字符串)。
所以应该是先输出SWIGStringHelperstatic constructor而后输出static method ofOsrPINVOKE。
试着用下面的代码调用一下:
static void Main(string[] args)
{
OsrPINVOKE.DoSomething();
Console.ReadLine();
}
却发现如果用的target framework是.net4,用release编译并且在VS外运行的话,就会只输出static method of OsrPINVOKE,感觉好像SWIGStringHelper的静态构造方法没有执行。而如果用的是.net 2.0、3.5,或者是用Debug编译或是在VS里面运行的话输出结果都和预期的一致。
难道是静态字段的初始化在.NET 4中变成Lazy的了?
事实证明真的是这样:
如果一个类型提供了显式声明的静态构造的话,那么这个静态构造方法会在创建该类型实例或者访问该类型的任何静态成员之前被执行。
如果一个类型没有提供显式声明的静态构造的话,编译器会自动给该类型一个默认的静态构造,并把静态字段的初始化都放到该默认静态构造中去,而这个默认的静态构造只会在静态字段被访问时才执行,也就是说创建实例、调用实例方法、调用静态方法时都不会触发静态构造的执行(当然前提是它们没有访问静态字段)。
不过CLR在加载一个类型时怎么知道其中包含的静态构造方法是编译器加上的还是原C#代码中显式提供的呢?事实上这是beforefieldinit的作用。
根据上面的原则再来分析一下:OsrPINVOKE中没有显式声明的静态构造,所以编译器会生成一个默认的静态构造并把helper的实例创建放入其中。而这个默认的静态构造只会在helper这个唯一的静态字段被访问时才会执行。而代码中没有任何地方访问了helper,所以OsrPINVOKE的静态构造根本就没有执行,helper根本就没有被new出来,SWIGStringHelper的静态构造自然也就没有执行。
所以要解决这个问题的话只要在OsrPINVOKE里面显式声明一个静态构造方法,把new SWIGStringHelper();这一句放到里面,或者仅仅是显式声明一个静态构造并把它留空。然后重新编译一下GDAL的Wrapper就可以了。
如果您也在.NET 4中调用GDAL时遇到了类似的问题,不妨试一下这种解决方法。
其实不仅是GDAL,其他由SWIG制作的托管Wrapper估计都会受到影响。