本文是读伟民哥翻译的 .NET内存管理宝典 这本书的笔记,我认为读书的过程也需要实践,这样对一知半解的知识也有较为清晰的了解。在阅读到 string 在内存的布局时,我看到 RuntimeHelpers 的 OffsetToStringData 数据,据说此属性可以获取到字符串的字符在内存存放的实际地址,本文将来写一个混合 C# 和 C++\CLI 的应用来进行测试
本文将完全采用 .NET 6 进行编写,分别创建 .NET 6 的 C# 控制台程序,和 .NET 6 的 C++\CLI 空项目。这里需要稍微说明的是 C++\CLI 是通过 C++ 编写的 .NET 应用程序,基于 .NET 运行时运行的程序
在 C++\CLI 项目里面添加一个叫 Foo 的类,在类里面添加一个方法,用来输出字符串的内容
namespace JuyurchelhiLewecujai
{
public ref class Foo
{
public:
static void Output(System::String^ input);
};
}
以上代码放在 Foo.h 文件里面,接下来实现 Output 方法。期望是在此方法里面获取在 .NET 定义的字符串对象的实际存放字符的内存指针,实现方法如下
#include "Foo.h"
#include <iostream>
#include "vcclr.h"
void JuyurchelhiLewecujai::Foo::Output(System::String^ input)
{
const pin_ptr<const wchar_t> p = PtrToStringChars(input);
wchar_t const* c = p;
wprintf(L"%s", c);
}
通过 VCClr 提供的 PtrToStringChars 方法可以取出 input 字符串里面的实际存放字符的指针,接着采用 pin_ptr
定住此对象。为什么需要采用 pin_ptr
定住?原因是 .NET 世界随时都会有 GC 将对象的地址变更,因此为了进行安全使用,需要使用 pin_ptr
定住此对象,这样在 GC 时就不会修改此对象的内存地址。细节请参阅 从C++到C++/CLI - feisky - 博客园
另一个细节是咱在 .NET 里面的字符串的编码格式都是 Unicode 也就是 U16 编码方式,需要对应到 wchar_t
类型,也需要使用 wprintf
输出而不能使用 printf
输出,否则将会读取到 \0
而只输出第一个字符。当然了,在 C++\CLI 项目里面依然是不推荐使用 iostream 进行输出的
那以上的 PtrToStringChars 是通过什么魔法进行实现的?可以看到此方法的实现如下
//
// get an interior gc pointer to the first character contained in a System::String object
//
inline __const_Char_ptr PtrToStringChars(__const_String_handle s) {
_Byte_ptr bp = const_cast<_Byte_ptr>(reinterpret_cast<__const_Byte_ptr>(s));
if( bp != _NULLPTR ) {
bp += System::Runtime::CompilerServices::RuntimeHelpers::OffsetToStringData;
}
return reinterpret_cast<__const_Char_ptr>(bp);
}
核心逻辑就是通过 RuntimeHelpers 的 OffsetToStringData 属性获取相对于字符串类型的地址的实际字符存放地址
尝试在 C# 项目里面调用刚才定义的 Foo 类型的 Output 代码,方法如下
class Program
{
static void Main(string[] args)
{
JuyurchelhiLewecujai.Foo.Output("Hello");
}
}
运行控制台项目,可以看到输出了 Hello 文本,这也就是说字符串的内存布局里面,存放字符数组的地方就是在距离字符串对象指针的 RuntimeHelpers.OffsetToStringData 的地方
然而在 .NET 5 和以上版本,标记了 OffsetToStringData 方法过时,官方推荐使用 GetPinnableReference 代替。关于 GetPinnableReference 请参阅 C#7.3 新增功能 - 张传宁 - 博客园
更改 C++\CLI 代码如下
void JuyurchelhiLewecujai::Foo::Output(System::String^ input)
{
auto pinString = &input->GetPinnableReference();
wprintf(L"%s", pinString);
}
可以通过如下方式获取本文的源代码,先创建一个空文件夹,接着使用命令行 cd 命令进入此空文件夹,在命令行里面输入以下代码,即可获取到本文的代码
git init
git remote add origin https://gitee.com/lindexi/lindexi_gd.git
git pull origin 414b803c3c4faa93d1075c28c85e5826c611d9cb
以上使用的是 gitee 的源,如果 gitee 不能访问,请替换为 github 的源
git remote remove origin
git remote add origin https://github.com/lindexi/lindexi_gd.git
获取代码之后,进入 CemholerecelQerrairdoway 文件夹
更多内存相关,我推荐伟明的 《.NET内存管理宝典 - 提高代码质量、性能和可扩展性》 这本书