如果是写过C++的肯定一提到字符就会想到字符编码(Multibyte或Unicode)。写.NET程序的便不会考虑这个,因为在CLR环境中默认会采用Unicode编码。(其实刚毕业的时候写了很久的.NET程序却完全不知道编码这回事,因为在.NET环境中好像不知道编码技术也不会对开发有什么影响。因此个人感觉出于对计算机科学系统学习,最好还是从非托管环境开始学习程序设计)。那在调用非托管的方法时,如何解决编码问题呢?答案是要用到DllImportAttribute的CharSet属性。
根据MSDN对CharSet的说明如下:
CharSet可能有:
Ansi
调用时将字符参数封送为Ansi编码的字符
Unicode
调用时将字符参数封送为Unicode编码的字符
Auto
字符的编码是运行时根据运行环境动态适应的(若是Unicode环境测用Unicode编码,否测用Ansi.在CLR环境下默认为Unicode)。
这里还有另外一个小细节,就是关于非托管方法的名字(在C++中很多方法都提供两个版本,比如:MessageBoxA,MessageW用来分别表示Ansi和Unicode版本的调用)。用DllImportAttribute的 ExactSpelling 属性来设定是否允许CLR根据CharSet的设定值去额外匹配对应的非托管函数中的相应版本的函数名。(具体可见MSDN)
Example:
Ansi版本:
C++ Code:
_declspec(dllexport) void _stdcall Print(char* msg)
{
if(msg)
printf("Message:%s",msg);
}
Native Method封装类 Code:
[DllImport("native.dll",EntryPoint = "Print")]
public static extern void Print(string msg);
// DllImport默认的CharSet为Ansi
托管调用Code:
NativeWrapper.Print("Hello World");
Unicode版本:
C++ Code:
_declspec(dllexport) void _stdcall PrintW(wchar_t* msg)
{
setlocale(LC_ALL,"chs");
if(msg)
wprintf(L"Message:%ls\n",msg);
}
Native Method封装类 Code:
[DllImport("native.dll", EntryPoint = "PrintW", CharSet = CharSet.Unicode)]
public static extern void PrintW(string msg);
//CharSet设为了Unicode,因为PrintWr接收的为宽字节字符
托管代码调用Code:
NativeWrapper.PrintW("你好");
关于ExactSpelling我始终没有尝试成功,不知是我使用不对还是怎么回事。如下代码:
C++ Code:
_declspec(dllexport) void _stdcall PrintW(wchar_t* msg)
{
setlocale(LC_ALL,"chs");
if(msg)
wprintf(L"Message:%ls\n",msg);
}
Native Method封装类 Code:
[DllImport("native.dll", EntryPoint = "Print", CharSet = CharSet.Unicode,ExactSpelling=false)]
public static extern void Print(string msg);
//CharSet设为了Unicode,因为PrintW接收的为宽字节字符
但是调用的时候总是报错说找不到Print Entry Point.其实写了这么多,太顺理成章了。有一个细节就被忽略了:C++接收的是指针,C#这边是string对象。为什么会工作正常呢?其实指针,对象都是浮云。如果
探究底层实现,指针就是用来表示一个内存地址的类型罢了,也就是这个变量的值是一个地址。而C#中的引用类型变量在栈上保存的就是对象在堆上的地址,当把这个
变量传给C++时,其实就是传了对象在堆上的地址(也就是C++中的指针)。
- 只读的CLR string
做了如下实验,将string传入c++,然后将内存中的值改掉:
C++代码:
_declspec(dllexport) void _stdcall ModifyText(char* originalText)
{
if(originalText)
strcpy(originalText,"Hello");
}
NativeWrapper代码:
[DllImport("native.dll", EntryPoint = "ModifyText")]
public static extern void ModifyText(string msg);
托管调用代码:
string originText = "Hello,World";
NativeWrapper.ModifyText(originText);
跟踪看到C++中代码确实改了originalText指针内存中的值,但是回到C#中originalText依然是原值。有点疑惑,有可能是string在CLR中的实现机制还
缺少研究。C#中string是只读的这一点是可以理解的,那为什么在C++中又可以修改呢。关键是修改了回到C#中为什么又恢复到原值了呢?莫非修改的不是C#中的内存,
但是string的值确实以指针的形式传进去了啊,有一种可能就是CLR传给C++的是一份拷贝,并不是originalText本身。待确认
补遗:确实是自己技艺不精,CLR在对参数进行封送的时候其实是将CLR中的堆中的数据拷贝一份到非托管堆中,然后会同步两块内存,但是此处string在CLR中是只读的
因此originalText仍然是原值。
- 封送为指定的字符指针类型
C++中为字符定了很多指针类型:LPSTR,LPWSTR,LPCSTR,LPCWSTR,LPTSTR,LPCTSTR等等。CLR默认会将string封送为LPSTR
那如何将C#中的字符指定封送为这些类型的指针从而满足C++函数的需求呢?答案是需要用到MarshalAsAttribute.
根据MSDN:
MarshalAs可以指定传入传出参数,返回值的封送目标类型。
Example:
C++代码:
_declspec(dllexport) void _stdcall Test4ConstStr(LPCWSTR pConstStr)
{
printf("Message:%s",pConstStr);
}
NativeWrapper代码:
[DllImport("native.dll", EntryPoint = "Test4ConstStr",CharSet= CharSet.Unicode)]
public static extern void Test4ConstStr([MarshalAs(UnmanagedType.LPWStr)]string constStr);
托管调用代码:
NativeWrapper.Test4ConstStr("你好");
- 对返回结果的封送
[DllImport("native.dll", EntryPoint = "Test4ConstStr",CharSet= CharSet.Unicode)]
[return:MarshalAs(UnManagedType.LPWStr)]
public static extern string Test4ConstStr([MarshalAs(UnmanagedType.LPWStr)]string constStr);