【发布时间】:2022-01-08 08:11:31
【问题描述】:
我有一个特殊的问题,我似乎无法在最小的工作示例中重现。 我必须处理一个大型遗留代码框架,并在我的范围之外修改所有这些。为了处理它,我必须应用一些特定的模式。
代码库概览
我有一个托管 C# 应用程序 (.NET 5.0)。在这个应用程序中,我需要运行一些 C++ 代码。 为此,有一个 CLI 包装器项目。这个包装器包含大部分遗留框架这是我无法控制的,这就是为什么我只能将字符串传输到我的 C++ 类(稍后会详细介绍)。基于配置,这个遗留框架使用包装器来实例化 C++ 类并调用它们的方法,处理结果最后销毁所有 C++ 类。 这个 CLI 包装器只允许我将字符串作为参数传递给它创建的 C++ 类。
我所有的库都是动态链接的(使用 DLL)。 C# 是一个引用 C++/CLI 包装器的项目,而后者又用我的 C++ 类引用了 C++ 项目。该项目引用了外部LargeLibrary(稍后会详细介绍)。
问题的根源
每隔几秒就会重复调用 C++ 代码。它应该快速响应。 我的 C++ 代码需要从磁盘加载一些大文件(大约 400 MB)并处理它,这需要相当长的时间。 由于每次都重新创建 C++ 类,因此每次加载文件都会消耗大量时间,这是不可接受的。 由于该数据基本上是恒定的,因此我尝试在程序初始化期间加载一次。然后我将一个指针传递给我的 C++ 类,然后它可以使用该对象。当 C++ 类被销毁时,该对象会保留在内存中,以便以后再次使用。
为了使事情复杂化,我需要一个相当大的库来读取和处理我的文件(我在此处将此库称为 LargeLibrary)。如果我使 CLI 包装器依赖于此,它将无法编译。
我可以想象这是因为 CLI 的东西。因此,我使用了void 指针,因此包装器不必知道指针后面的实际类型。实际对象是使用我的 C++ 类中的函数创建的(因此正确的析构函数链接到共享指针)。
这一切都编译得很好。
我的解决方案
我对 CLI 包装器做了一个小扩展,以创建从磁盘读取我的文件并将信息保存在内存中的对象。
该对象是使用CreateInformationObject() 方法创建的。 ptr_native 是一个智能指针,用于在托管代码中使用本机对象。它的类型是:CAutoNativePtr<std::shared_ptr<void>> ptr_native。
在包装器中创建我的对象如下所示:
// Create a shared_ptr on dynamic memory (i.e. heap).
std::shared_ptr<void>* objectPointer = new std::shared_ptr<void>();
// Load the module and store a shared pointer pointing to it in the dynamic memory.
*objectPointer = CppConsumerStuff::CppConsumer::CreateInformationObject(value);
// Load the module and store a shared pointer pointing to it in the dynamic memory.
ptr_native.Attach(objectPointer);
我的 C++ 类(CppConsumerStuff::CppConsumer)中的 CreateInformationObject() 方法是:
std::shared_ptr<void> CppConsumer::CreateInformationObject(std::string pathToFile)
{
std::shared_ptr<LargeLibrary::ActualObjectType> objectPtr = std::make_shared<LargeLibrary::ActualObjectType>();
*objectPtr = LargeLibrary::FileLoader::load(pathToFile)
return objectPtr;
}
然后,由于遗留框架,我尝试了这个长镜头:将指针地址转换为string,通过框架将其传递给我的 C++ 类,并将其转换回指向对象实际类型的指针。
这就像(在我的 CLI 包装器扩展中):
//Cast void pointer to string.
String^ CliStorage::GetPointerString()
{
std::stringstream ss;
ss << (*ptr_native).get(); // Pointer to hex string.
std::string ptr_string = ss.str();
return StringToManaged(ptr_string);
}
最后,(在我的 C++ 类中),我将此指针字符串转换回指向实际对象的指针:
void DoWorkOnLargeObject(std::string ptr_string)
{
// Cast pointer to usable type
uint64_t raw_ptr = 0; // Define int size depending on system architecture.
std::stringstream ss;
ss << std::hex << ptr_string;
ss >> raw_ptr; //Hex string to int.
cppObjectPtr = reinterpret_cast<void*>(raw_ptr);
LargeLibrary::ActualObjectType* cppObjectPtrCasted = static_cast<LargeLibrary::ActualObjectType*>(cppObjectPtr);
// Use the object.
cppObjectPtrCasted->GetDataStuff();
// Rest of code doing work...
}
我的结果
我在 Visual Studio 2019 中构建所有这些。 当我创建一个调试版本时,一切正常:)。 但是,当我创建发布版本时,它不起作用并引发以下异常:``
最小的工作示例
我尝试创建一个最小的工作示例。 有和没有大型外部库。 但是,在我的最低工作示例中,无论构建类型(调试/发布)如何,它始终有效。
我的问题
所以我的问题是:我的最低工作示例是否偶然起作用,我是否依赖于未定义的行为?或者这个概念(不管它有多丑陋)是否真的有效? 如果是未定义的行为,请解释一下,我想学习。如果它应该工作,问题在于遗留框架,我会对此进行调查。
我知道这些是非常丑陋的模式,但我尝试在我的范围内使用我拥有的方法。
谢谢
编辑,我在我的问题中添加了 CreateInformationObject() 方法代码。我想我的危险可能就在这里。也许我做了一些导致未定义行为的非法指针?
【问题讨论】:
-
std::shared_ptr<void>*是一个主要的反模式。std::shared_ptr只能用作自动变量或成员变量,绝不可用作指针,也不应被堆分配。这破坏了使用智能指针的全部价值。理想情况下,当您在 C++ 中使用字符串时,您不会将它们从宽字符串转换,这样做时您会丢失信息。请改用std::wstring。另外,是的,您依赖于大量未定义的行为。这段代码非常不安全。 -
注意:
CAutoNativePtr不是必需的,如果您只是将std::shared_ptr<LargeLibrary::ActualObjectType>作为应该可以正常工作的 C++/CLI 引用类的数据成员。默认情况下,它将使 C++/CLI 类型为 Disposable,因为它会调用一个析构函数,但这很好。如果您正确使用智能指针,则不需要手动实现析构函数。我确实担心您可能不需要也不想要shared_ptr,但没有足够的信息可以确定。 -
我是否理解正确:您的程序在堆的某个点写入一些数据并将该位置保存在某处。然后它退出。接下来,这个程序的一个新实例(即不是在堆上创建分配指针的实例)从某个地方读取位置并尝试访问它。这是正确的吗?我看到一个程序不允许从任意内存位置读取以保护其他程序的数据的问题(en.wikipedia.org/wiki/Memory_protection)。因此,这在我看来是行不通的。
-
@Mgetz ,我已将方法
CreateInformationObject的定义添加到我的问题中。如您所见,我创建了shared_ptr,这样当持有共享指针的类超出范围时,它知道要调用哪个析构函数。你还觉得有什么不对吗? -
我认为将指针保存到堆然后稍后通过 reinterpret_cast 使用它的方法将导致 UB。我没有考虑标准的特定部分,但我认为您所处的区域必须证明它是有效的,而不是相反。此外,即使只有一个进程,我认为共享内存的方法也是有效的。话虽这么说也许std::launder可以帮助你。有关说明,请参阅 stackoverflow.com/questions/39382501/…。