31

我编写了一些代码,利用开源库来完成一些繁重的工作。这项工作是在 linux 中完成的,使用单元测试和 cmake 来帮助将其移植到 Windows。需要让它在两个平台上运行。

我喜欢 Linux,我喜欢 cmake,我喜欢我可以自动生成 Visual Studios 文件。就像现在一样,在 Windows 上,一切都将编译,它将链接并生成测试可执行文件。

然而,为了达到这一点,我不得不与 windows 斗争了几天,学习所有关于清单文件和可再发行包的知识。

据我的理解:

在 VS 2005 中,Microsoft 创建了 Side By Side dll。这样做的动机是,以前,多个应用程序会安装同一个 dll 的不同版本,导致以前安装和工作的应用程序崩溃(即“Dll Hell”)。并排 dll 解决了这个问题,因为现在每个可执行文件/dll 都附加了一个“清单文件”,用于指定应该执行哪个版本。

这一切都很好。应用程序不应再神秘地崩溃。然而...

Microsoft 似乎在每次发布 Visual Studios 时都会发布一组新的系统 dll。另外,正如我之前提到的,我是一名试图链接到第三方库的开发人员。通常,这些东西作为“预编译的 dll”分发。现在,当使用一个版本的 Visual Studio 编译的预编译 dll 链接到使用另一个版本的 Visual Studio 的应用程序时会发生什么?

根据我在互联网上阅读的内容,发生了不好的事情。幸运的是,我从来没有走到那一步——在运行可执行文件时,我一直遇到“MSVCR80.dll not found”问题,因此我开始涉足整个清单问题。

我最终得出的结论是,让它工作的唯一方法(除了静态链接一切)是所有第三方库必须使用相同版本的 Visual Studios 编译 - 即不要使用预编译的 dll - 下载源代码,构建一个新的 dll 并改用它。

这是真的吗?我错过了什么?

此外,如果情况似乎如此,那么我不禁认为微软出于邪恶的原因故意这样做。

它不仅会破坏所有预编译的二进制文件,从而使使用预编译的二进制文件变得不必要地困难,如果您碰巧在一家使用第三方专有库的软件公司工作,那么每当他们升级到最新版本的视觉工作室时 - 您的公司必须现在做同样的事情,否则代码将不再运行。

顺便说一句,linux如何避免这种情况?虽然我说我更喜欢在它上面进行开发并且我了解链接的机制,但我没有维护任何应用程序足够长的时间来遇到这种低级共享库版本控制问题。

最后,总结一下:这种新的清单方案是否可以使用预编译的二进制文件?如果是,我的错误是什么?如果不是,微软真的认为这会使应用程序开发更容易吗?

更新 - 一个更简洁的问题:Linux 如何避免使用 Manifest 文件?

4

4 回答 4

27

应用程序中的所有组件必须共享相同的运行时。如果不是这种情况,您会遇到奇怪的问题,例如在删除语句上断言。

这在所有平台上都是一样的。这不是微软发明的东西。

您可以通过了解运行时可能会反击的位置来解决这个“只有一个运行时”的问题。这主要是在您在一个模块中分配内存并在另一个模块中释放它的情况下。

a.dll
    dllexport void* createBla() { return malloc( 100 ); }

b.dll
    void consumeBla() { void* p = createBla(); free( p ); }

当 a.dll 和 b.dll 链接到不同的 rumtime 时,这会崩溃,因为运行时函数实现了自己的堆。

您可以通过提供必须调用以释放内存的 destroyBla 函数轻松避免此问题。

您可能会在运行时遇到一些问题,但大多数都可以通过包装这些构造来避免。

以供参考 :

  • 不要跨模块边界分配/释放内存/对象
  • 不要在你的 dll 接口中使用复杂的对象。(例如 std::string, ...)
  • 不要跨 dll 边界使用复杂的 C++ 机制。(类型信息,C++ 异常,...)
  • ...

但这不是清单的问题。

清单包含模块使用的运行时的版本信息,并由链接器嵌入到二进制文件 (exe/dll) 中。当一个应用程序被加载并且它的依赖被解析时,加载器查看嵌入在 exe 文件中的清单信息,并使用 WinSxS 文件夹中相应版本的运行时 dll。您不能只将运行时或其他模块复制到 WinSxS 文件夹。您必须安装 Microsoft 提供的运行时。当您在测试/最终用户机器上安装软件时,可以执行 Microsoft 提供的 MSI 包。

因此,在使用您的应用程序之前安装您的运行时,您将不会收到“缺少依赖项”错误。


(更新为“Linux如何避免使用Manifest文件”问题)

什么是清单文件?

引入清单文件以将消歧信息放置在现有的可执行/动态链接库旁边或直接嵌入到该文件中。

这是通过指定启动应用程序/加载依赖项时要加载的 dll 的特定版本来完成的。

(您可以对清单文件执行其他几项操作,例如,可以将一些元数据放在此处)

为什么这样做?

由于历史原因,该版本不是 dll 名称的一部分。所以“comctl32.dll”在它的所有版本中都是这样命名的。(所以Win2k下的comctl32与XP或Vista下的comctl32不同)。要指定您真正想要的版本(并且已经测试过),请将版本信息放在“appname.exe.manifest”文件中(或嵌入此文件/信息)。

为什么这样做?

许多程序将它们的 dll 安装到 systemrootdir 上的 system32 目录中。这样做是为了允许为所有相关应用程序轻松部署共享库的错误修复。在内存有限的日子里,当多个应用程序使用相同的库时,共享库减少了内存占用。

这个概念被许多程序员滥用,当他们将所有的dll都安装到这个目录中时;有时会用较旧的共享库覆盖较新版本的共享库。有时库的行为会悄无声息地改变,从而导致相关应用程序崩溃。

这导致了“分发应用程序目录中的所有dll”的方法。

为什么这很糟糕?

当出现错误时,必须更新分散在几个目录中的所有 dll。(gdiplus.dll) 在其他情况下,这甚至是不可能的(Windows 组件)

显式方法

这种方法解决了上述所有问题。您可以将 dll 安装在程序员可能不会干预的中心位置。在这里可以更新 dll(通过更新 WinSxS 文件夹中的 dll)并且加载程序加载“正确”的 dll。(版本匹配由 dll-loader 完成)。

为什么 Linux 没有这种机制?

我有几个猜测。(这真的只是猜测......)

  • 大多数东西都是开源的,因此重新编译错误修复对目标受众来说不是问题
  • 因为只有一个“运行时”(gcc 运行时),所以运行时共享/库边界的问题不会经常发生
  • 许多组件在接口级别使用 C,如果操作正确,这些问题就不会发生
  • 在大多数情况下,库的版本嵌入在其文件名中。
  • 大多数应用程序都静态绑定到它们的库,因此不会发生 dll-hell。
  • GCC 运行时保持非常 ABI 稳定,因此不会发生这些问题。
于 2009-02-26T02:00:47.893 回答
3

如果第三方 DLL 将分配内存并且需要释放它,那么您需要相同的运行时库。如果 DLL 有 allocate 和 deallocate 函数,就可以了。

如果第三方 DLL 使用std容器,例如vector等,您可能会遇到问题,因为对象的布局可能完全不同。

有可能让事情正常工作,但有一些限制。我遇到了上面列出的两个问题。

于 2009-02-26T15:09:08.357 回答
3

如果第三方 DLL 分配了您需要释放的内存,则 DLL 违反了交付预编译 DLL 的主要规则之一。正是因为这个原因。

如果 DLL 仅以二进制形式发布,那么它还应发布与其链接的所有可再发行组件,并且其入口点应将调用者与任何潜在的运行时库版本问题(例如不同的分配器)隔离开来。如果他们遵守这些规则,那么你就不应该受苦。如果他们不这样做,那么您将遭受痛苦和折磨,或者您需要向第三方作者投诉。

于 2009-02-26T16:40:01.273 回答
1

我最终得出的结论是,让它工作的唯一方法(除了静态链接一切)是所有第三方库必须使用相同版本的 Visual Studios 编译 - 即不要使用预编译的 dll - 下载源代码,构建一个新的 dll 并改用它。

或者(以及我们必须在我工作的地方使用的解决方案)是,如果您需要使用的所有第三方库都是使用相同的编译器版本构建(或作为构建可用),您可以“只”使用该版本。例如,“必须”使用 VC6 可能会很麻烦,但是如果有一个您必须使用的库并且它的源不可用,而这就是它的来源,那么您的选择就会很遗憾地受到限制。

……据我了解。:)

(我的工作不在 Windows 中,尽管我们不时从用户的角度与 Windows 上的 DLL 作斗争,但是我们确实必须使用特定版本的编译器并获取全部使用相同的编译器。谢天谢地,所有供应商都倾向于保持最新,因为他们多年来一直在提供这种支持。)

于 2009-02-26T01:30:29.650 回答