字符串的封送
不同风格的字符串
由于字符串是non-blittable类型,所以在平台调用时,如果非托管函数使用了字符串,那么就需要对其进行封送。
C风格字符串:实际上是一个字符数组,以\0
终止,所以比实际字符串多一个字符。
Pascal风格字符串:也采用字符数组方式,但是显式地将字符串长度存储在字符串的最前面。
由于C风格字符串容易出错且不宜使用,C++中出现了很多字符串封装类,比如
std::string
、CString
等。
BSTR字符串:许多COM接口和OLE使用BSTR
定义字符串。BSTR
是Pascal风格字符串和C风格字符串的混合型字符串。BSTR是包含Unicode的字符串,其长度预设在字符串前面,并且用一个0字符作为结束标记。其长度是一个DWORD
类型的值,表示的是不包含结尾0字符的总字节数。而C++中的BSTR实际上是直线字符串首字符的指针,其定义为typedef OLECHAR* BSTR
。
C#的String:dotnet将String实现为对象,且将其归为引用类型。字符串对象是不可改变的(Immutabe)。
- C风格字符/字符串重定义对照说明:
类型 | 对于何种类型的重定义 | 说明 |
---|---|---|
WCHAR |
wchar_t |
Unicode字符 |
TCHAR |
平台相关char 或wchar_t |
ANSI或者MBCS或者Unicode |
LPSTR |
char* |
以null终止的ANSI或者MBCS字符串 |
LPCSTR |
const char* |
以null终止的ANSI或者MBCS字符串常量 |
LPWSTR |
WCHAR** |
以null终止的Unicode字符串 |
LPCWSTR |
const WCHAR** |
以null终止的Unicode字符串常量 |
OLECHAR |
wchar_t |
Unicode字符 |
LPOLESTR |
OLECHAR* |
OLECHAR字符串 |
LPCOLESTR |
const OLECHAR* |
OLECHAR字符串常量 |
- C++字符串类及其类型对照表:
字符串类 | 字符串类型 | 可以转化为何种字符串 |
---|---|---|
_bstr_t |
BSTR |
const wchar_t* |
_variant_t |
BSTR |
ANSI、MBCS、Unicode字符 |
string |
MBCS |
const char* (调用c_str()函数) |
wstring |
Unicode |
const wchar_t* (调用c_str()函数) |
CComBSTR |
BSTR |
const wchar_t 或者BSTR |
CComVariant |
BSTR |
const wchar_t 或者BSTR(使用ChangeType(),然后获取VARIANT的成员bstrVal) |
CString |
TCHAR |
在ANSI或MBCS下,可以转换为const char* ,在Unicode下可以转换为const wchar_t* |
COleVariant |
BSTR |
const wchar_t* 或者BSTR(使用ChangeType(),然后获取VARIANT的成员bstrVal) |
封送作为参数的字符串
- 通过”CharSet”以及”方向属性”控制封送行为
示例:
void _cdecl Test(
const wchar_t* inString,
wchar_t* outString,
int bufferSize
){
wcscpy_s(outString, bufferSize, inString);
}
[DllImport("NativeLib.dll", CharSet = CharSet.Unicode, CallConvertion = CallingConvertion.Cdecl)]
private extern static void Test(string inString, StringBuilder outString, int bufferSize);
从非托管函数的定义可以看出,第一个字符串是传入参数,第二个字符串是传出参数。这意味着在非托管函数中,将不会对第一个参数的值做任何修改,而会修改第二个参数返回结果。
因此,为了将作为方法参数的字符串数据类型封送到非托管代码,在选择数据类型时,第一个参数采用string,因为它正好是是Immutable的,而第二个参数采用StringBuilder,因为它是一个可修改的字符串缓冲区。
默认情况下,通过值传递的引用类型出于性能原因只能作为In参数封送。因此采用string类型之后,在调用返回时就不会将字符串从非托管内存复制回托管内存,提高了效率。
当应用于方法参数和返回值时,In和Out属性能够控制封送处理的方向,所以它们称为“方向属性”。In属性告诉CLR在调用开始时将数据从调用方封送到被调方。而Out属性告诉CLR在返回时将数据从被调方封送回调用方。
In和Out属性可以单独使用,也可以组合使用,还可以完全不使用。如果跟没有指定,CLR就会使用默认方向属性。默认情况下通常都是In属性。但是如果是StringBuilder
类,会被CLR作为特殊类型区别对待,默认同时使用In和Out属性。
此外,C#中的out
和ref
关键字可能会更改已应用的方向属性。(out
->[Out]
; ref
-> [In,Out]
)
需要注意的是,某些情况下Out属性将被忽略。最常见的情况是Out属性被用于Blittable数据类型。由于Blittable数据类型会被封送拆收器锁定,实际传递的是指向托管对象的指针,因此非托管代码对指针所指数据结构的任何更改都能被托管代码及时获得。
StringBuilder
的使用最好遵循以下规则:一是不要通过out/ref
传递StringBuilder
。否则会被CLR认为是wchar_t**
类型,同时CLR也无法锁定StringBuilder的内部缓冲区,大大降低性能。二是非托管函数使用Unicode时,最佳做法是使用StringBuilder
作为Unicode的LPARRAY
或者LPWSTR
封送,因为字符串复制和转ANSI会降低性能。三是始终提前指定StringBuilder
的容量,并确保该容量能够足够存放缓冲区。最好把字符串缓冲区大小作为函数参数防止溢出。
- 使用MarshalAs特性控制字符串封送行为
除了使用CharSet指定封送操作之外,还可以使用MarshalAs
特性来指定某个字符串类型参数的封送处理方式。二者的区别是CharSet影响整个函数调用的字符串封送,而MarshalAs
只影响其作用的字符串参数。
枚举类型 | 非托管格式的说明 |
---|---|
UnmanagedType.AnsiBStr |
具有预设长度并包含ANSI字符的COM样式的BSTR |
UnmanagedType.BStr |
具有预设长度并包含Unicode字符的COM样式的BSTR |
UnmanagedType.LPStr |
指向以null终止的ANSI字符数组的指针 |
UnmanagedType.LPTStr (默认) |
指向以null终止的、平台相关的字符数组的指针 |
UnmanagedType.LPWStr |
指向以null终止的Unicode字符数组的指针 |
UnmanagedType.TBStr |
具有预设长度并包含平台相关字符的COM样式的BSTR |
VBByRefStr |
该值使VB.NET能够更改非托管代码中的字符串,并使修改结果能在托管代码中反映出来。该值只支持平台调用的情况 |
此表只适用于
string
类型,对StringBuilder
而言,能允许的选项只有LPStr
、LPTStr
、LPWStr
。
ref/out
能够改变CLR对象的封送处理行为
C#签名 | 非托管签名 | IL签名 | CLR中的实际IL签名 |
---|---|---|---|
int |
int |
int |
[in] int |
out int |
int* |
[out] int & |
[out] int & |
ref int |
int* |
int & |
[in, out] int & |
string |
char* |
string |
[in] string |
out string |
char** |
[out] string & |
[out] string & |
ref string |
char** |
string & |
[in, out] string & |
- 引用修改和就地修改
函数调用期间,函数可以对其参数进行两种类型的修改:引用修改和就地修改(in-place)。引用修改主要改变指针指向的位置(会先释放原来指向的内存防止内存泄漏)。就地修改则是改变引用所指向位置的内存。进行哪一种主要取决于参数类型以及调用和被调用方的约定。
C#签名 | 更改类型 |
---|---|
out int |
就地修改 |
ref int |
就地修改 |
out string |
引用修改 |
ref string |
引用修改或就地修改 |
[In,Out]StringBuilder |
就地修改 |
- 内存的释放
如果非托管函数对参数进行的是引用修改,也就是说,非托管函数分配了内存,并且将字符串指针指向的位置改为新分配的内存。这就必须要求完成平台调用后,释放掉由非托管函数分配的内存。
前面讲到过在托管代码中释放非托管内存的方法。建议在编写非托管函数时,采用CoTaskMemAlloc
分配内存,这样封送拆封器在释放非托管内存时,就能自动调用CoTaskMemFree
释放非托管内存。这就无需再为释放非托管内存而进行额外处理,减轻了复杂度。
封送作为返回值的字符串
如果字符串作为非托管函数的返回值封送到托管代码,则需要进行一些诸如释放非托管内存的额外处理。
- 使用IntPtr类型
wchar_t* _cdecl Test(int id)
{
int buffserSize = 64;
wchar_t* result = (wchar_t*)CoTaskMemAlloc(bufferSize);
//...
return result;
}
[DllImport("NativeLib.dll", CharSet = CharSet.Unicode, CallingConvertion = CallingConvertion.Cdecl, EntryPoint = "Test")]
private extern static IntPtr Test(int id);
string result;
IntPtr strPtr = Test(1);
result = Marshal.PtrToStringUni(strPtr);
Marshal.FreeCoTaskMem(strPtr);
Console.WriteLine(result);
由于采用了
IntPtr
类型封送非托管指针,因此必须手动从IntPtr
中的非托管内存中获取需要的字符串,然后释放非托管内存。
- 使用string类型
如果改用string
类型封送非托管函数返回的字符串,那么手动封送数据和释放非托管内存这两步操作都会由封送拆收器自动完成,从而无需在托管代码中手动处理。
[DllImport("NativeLib.dll", CharSet = CharSet.Unicode, CallingConvertion = CallingConvertion.Cdecl, EntryPoint = "Test")]
private extern static string Test(int id);
//..
string result = Test(1);
Console.WriteLine(result);
封送BSTR类型的字符串
BSTR是一种特殊类型的字符串,因此BSTR类型字符串封送处理时,对数据封送和内存释放需要进行一些不同的处理。并且不能再使用string
类型进行封送。
BSTR _cdecl Test(BSTR* ppString)
{
*ppString = NULL:
*ppString = SysAllocString(L"BSTR form arg");
BSTR result = SysAllocString(L"Return BSTR");
return result;
}
[DllImport("NativeLib.dll", CharSet = CharSet.Unicode, CallingConvertion = CallingConvertion.Cdecl )]
private extern static IntPtr Test(out IntPtr bstrPtr);
//...
private static void Foo()
{
IntPtr pStr = IntPtr.Zero;
IntPtr result = IntPtr.Zero;
result = Test(out pStr);
string argStr = Marshal.PtrToStringBSTR(pStr);
string retStr = Marshal.PtrToStringBSTR(result);
Marshal.FreeBSTR(result);
}
由于C++中的BSTR
实际上是对OLECHAR*
的重定义,所以BSTR实际都是Unicode字符。这样就需要CharSet.Unicode
托管声明,由于BSTR
的特殊性,还必须使用IntPtr
来封送,而且还必须调用专门用于释放BSTR内存的方法释放掉非托管内存。
(END)