【问题标题】:Using shared libraries vs a single executable使用共享库与单个可执行文件
【发布时间】:2009-09-16 12:53:12
【问题描述】:

我的同事声称我们应该将 C++ 应用程序(C++、Linux)分解为共享库,以提高代码模块化、可测试性和重用性。

在我看来,这是一种负担,因为我们编写的代码不需要在同一台机器上的应用程序之间共享,也不需要动态加载或卸载,我们可以简单地链接一个单一的可执行应用程序。

此外,恕我直言,用 C 函数接口包装 C++ 类使其更丑陋。

我还认为,在客户站点远程升级单文件应用程序会更容易。

在不需要在应用程序之间共享二进制代码且无需动态代码加载的情况下,是否应该使用动态库?

【问题讨论】:

  • 不,它没有。这听起来像是一个可以区分好答案和坏答案的问题。因此,写出好答案的人应该得到代表奖励。当所有答案都相同时使用 CW(例如,在投票或笑话线程中)
  • 在组件之间设置硬边界以防止意外或懒惰地创建依赖项是有意义的。在您的情况下,将应用程序拆分为静态链接库就足够了,这样您就可以保留单个可执行文件并且仍然具有单独的编译单元。
  • 没有必要为任何东西编写 C 包装器。

标签: c++ c shared-libraries


【解决方案1】:

我会说将代码拆分为共享库以改进而不考虑任何直接目标是流行语出没的开发环境的标志。最好编写可以在某些时候轻松拆分的代码。

但是为什么需要将 C++ 类包装到 C 函数接口中,除了,也许是为了创建对象?

此外,此处拆分为共享库听起来像是一种解释性语言的思维方式。在编译语言中,您尽量不要将您在编译时可以做的事情推迟到运行时。不必要的动态链接就是这种情况。

【讨论】:

  • 编译器的不兼容性使得在共享库接口中不使用任何 C++ 特性变得更容易。例如,如果在客户端以不同的方式实现类的布局(例如,不同的对齐方式),则由其编译器编译的代码会导致错误应用于您的数据结构。一些旧编译器也存在 vtable 问题。
  • Basilevs,这通常是正确的,但据我所知,它绝不适用于此案。
  • @Bavilevs:解决方案是提供两个 DLL。在 C++ DLL 上,客户端代码将在同一个编译器上使用它,而另一个为第一个编译器提供 C 接口。无需通过强加 C 接口来惩罚共享相同编译器的客户端。
  • @Basilevs:黑客是对的:在当前情况下,编译器是已知的,并且对于所有静态库都是唯一的。如果他们尝试“共享库”解决方案,我认为没有理由改变这一点。因此,基本上,共享库之间不需要 C 接口。恕我直言,这个“C 接口”参数表明问题作者不熟悉共享库。
【解决方案2】:

强制共享库可确保库没有循环依赖。与在最终应用程序链接之前没有任何链接相比,使用共享库通常会导致更快的链接和链接错误在更早的阶段被发现。如果您想避免向客户发送多个文件,您可以考虑在开发环境中动态链接应用程序,并在创建发布版本时静态链接。

编辑:我真的不明白为什么你需要使用 C 接口来包装你的 C++ 类 - 这是在幕后完成的。在 Linux 上,您可以使用共享库而无需任何特殊处理。但是,在 Windows 上,您需要 ___declspec(export) 和 ___declspec(import)。

【讨论】:

  • 至少在某些系统上,您可以拥有包含未解析符号的共享库。因此,可以创建一组共享库,这样它们必须全部链接到一个应用程序。
【解决方案3】:

提高重用性,即使没有?听起来不是一个强有力的论点。

代码的模块化和可测试性不必依赖于最终部署的单元。我希望链接是一个较晚的决定。

如果您确实有一个可交付成果,并且从未预料到会对其进行任何更改,那么分段交付听起来有点矫枉过正和不必要的复杂性。

【讨论】:

    【解决方案4】:

    简短的回答:不。

    更长的答案:动态库不会为测试、模块化或重用增加任何东西,而这些在单体应用程序中无法轻松完成。关于我能想到的唯一好处是,这可能会迫使在没有纪律的团队中创建 API。

    库(动态或其他)没有什么神奇之处。如果您拥有构建应用程序和各种库的所有代码,您可以轻松地将它们全部编译到一个可执行文件中。

    一般来说,我们发现处理动态库的成本是不值得的,除非有迫切的需求(多个应用程序中的库,需要在不重新编译的情况下更新多个应用程序,使用户能够向应用程序添加功能)。

    【讨论】:

      【解决方案5】:

      剖析你同事的论点

      如果他认为将您的代码拆分为共享库会提高代码的模块化、可测试性和重用性,那么我猜这意味着他认为您的代码存在一些问题,并且强制执行“共享库”架构会纠正它.

      模块化?

      您的代码必须具有不希望的相互依赖关系,如果将“库代码”和“使用库代码的代码”分开,则不会发生这种情况。

      现在,这也可以通过静态库来实现。

      测试?

      可以更好地测试您的代码,也许为每个单独的共享库构建单元测试,并在每次编译时自动化。

      现在,这也可以通过静态库来实现。

      代码复用?

      您的同事希望重用一些未公开的代码,因为这些代码隐藏在您的单体应用程序的源代码中。

      结论

      第 1 点和第 2 点仍然可以通过静态库实现。 3 将使共享库成为强制性的。

      现在,如果您有多个库链接深度(我正在考虑将两个静态库链接在一起,这些静态库已经编译链接其他库),这可能会很复杂。在 Windows 上,这会导致链接错误,因为某些函数(通常是 C/C++ 运行时函数,当静态链接时)被多次引用,并且编译器无法选择调用哪个函数。我不知道这在 Linux 上是如何工作的,但我想这也可能发生。

      剖析你自己的论点

      你自己的论点有些偏颇:

      编译/链接共享库的负担?

      与编译和链接到静态库相比,编译和链接到共享库的负担是不存在的。所以这个论点没有任何价值。

      动态加载/卸载?

      在非常有限的用例中,动态加载/卸载共享库可能是个问题。在正常情况下,操作系统会在需要时加载/卸载库而无需您的干预,无论如何,您的性能问题存在于其他地方。

      使用 C 接口公开 C++ 代码?

      至于为您的 C++ 代码使用 C 函数接口,我无法理解:您已经将静态库与 C++ 接口链接在一起。链接共享库也不例外。

      如果您有不同的编译器来生成应用程序的每个库,您会遇到问题,但情况并非如此,因为您已经静态链接了您的库。

      单个文件二进制更容易?

      你是对的。

      在 Windows 上,差异可以忽略不计,但仍然存在 DLL Hell 的问题,如果您将版本添加到库名称或使用 Windows XP,该问题就会消失。

      在 Linux 上,除了上述 Windows 问题之外,您还知道默认情况下,共享库需要位于某些系统默认目录中才能使用,因此您必须在安装时将它们复制到那里(可能会很痛苦...)或更改一些默认环境设置(这也可能很痛苦...)

      结论:谁是对的?

      现在,您的问题不是“我的同事是对的吗?”。他是。你也是。

      你的问题是:

      1. 您真正想要实现什么目标?
      2. 这项任务所需的工作是否值得?

      第一个问题非常重要,因为在我看来,你的论点和你同事的论点都有偏见,导致得出对你们每个人来说似乎更自然的结论。

      换一种说法:你们每个人都已经知道理想的解决方案应该是什么(根据每个观点),并且你们每个人都为达到这个解决方案而积累了论据。

      没有办法回答这个隐藏的问题......

      ^_^

      【讨论】:

      • tl;dra (太长;确实读过) OP 并不建议他们链接静态库,并且没有这样的提示,应该假设他们通过链接在一起生成可执行文件大量的目标文件。此外,在我看来,OP 可能偏向于(下意识地)将静态库和共享库混为一谈;我很难相信他的同事不知道(大部分)他所追求的好处可以通过使用静态库来实现。
      【解决方案6】:

      进行简单的成本/收益分析 - 您真的需要模块化、可测试性和重用性吗?你有时间花时间重构你的代码来获得这些特性吗?最重要的是,如果您进行重构,您获得的收益是否可以证明执行重构所花费的时间是合理的?

      除非您现在在测试方面遇到问题,否则我建议您保持原样。模块化很棒,但 Linux 有自己的“DLL 地狱”版本(请参阅 ldconfig),并且您已经指出重用不是必需的。

      【讨论】:

      • 谁不需要可测试性?
      【解决方案7】:

      如果您提出问题但答案不明显,请留在原处。如果您还没有达到构建单体应用程序花费太长时间或者让您的团队一起工作太痛苦的地步,那么没有令人信服的理由转向库。如果您愿意,您可以构建一个适用于应用程序文件的测试框架,或者您可以简单地创建另一个使用相同文件的项目,但附加一个测试 API 并使用它构建一个库。

      出于交付目的,如果您想构建库并交付一个大型可执行文件,您始终可以静态链接到它们。

      如果模块化有助于开发,即您总是与其他开发人员就文件修改发生冲突,那么库可能会有所帮助,但这也不能保证。无论如何,使用良好的面向对象的代码设计都会有所帮助。

      并且没有理由使用创建库所需的 C 可调用接口来包装任何函数,除非您希望它可以从 C 中调用。

      【讨论】:

        【解决方案8】:

        共享库让人头疼,但我认为共享库是正确的方法。我想说,在大多数情况下,您应该能够使您的应用程序的一部分模块化并在您的业务中的其他地方可重用。此外,根据这个单一可执行文件的大小,只上传一组更新的库而不是一个大文件可能更容易。

        IMO,库通常会带来更好的代码、更可测试的代码,并允许以更有效的方式创建未来的项目,因为您无需重新发明轮子。

        简而言之,我同意你同事的观点。

        【讨论】:

        • 但是模块化可能永远不会被重用的代码的意义在哪里?我会同意模块化,但只是到了值得付出努力的程度..
        • IMO,我发现在大多数情况下模块化是值得的,尤其是在谈论“单体”应用程序时。我发现将东西编译到库中并在我的代码中使用它们并不费时。为什么库没有什么神奇的作用,我觉得它们有助于使依赖关系更加明显,导致更好的可测试性、更快的编译时间和更好的环境。稍后如果代码最终再次被使用,何时以及谁决定将其作为库?程序员会花时间去做,还是只是将文件复制到他/她的项目中并编译?尽早做可以避免一团糟。
        【解决方案9】:

        在 Linux(和 Windows)上,您可以使用 C++ 创建共享库,而不必使用 C 函数导出来加载它。

        也就是说,您将 classA.cpp 构建到 classA.so 中,并将 classB.cpp 构建到链接到 classA.so 的 classB(.exe) 中。您真正要做的就是将您的应用程序拆分为多个二进制文件。这确实具有编译速度更快、更易于管理的优点,并且您可以编写仅加载该库代码进行测试的应用程序。

        一切仍然是 C++,一切都链接,但您的 .so 与静态链接的应用程序是分开的。

        现在,如果您想在运行时加载不同的对象(也就是说,直到运行时才知道要加载哪个对象),那么您需要使用 c-exports 创建一个共享对象,但您也将手动加载这些功能;您将无法使用链接器为您执行此操作。

        【讨论】:

          【解决方案10】:

          首先,让我免除您问题中的错误假设。你不需要用 C 接口包装你的 C++。

          现在让我们来看看案例。

          缺点:

          • 将您的应用程序分解为模块是一项工作。
          • 您将(可能)发现相互依赖关系,使其更加有效。
          • 您必须确保库在运行时进入正确的位置。与 Windows 上的 exe 相同的目录,在 Linux/*nix 上位于 LD_LIBRARY_PATH 的某个位置。

          非问题:

          • 性能。加载后,调用 dll 中的函数应该是零性能损失。它甚至可以提高某些系统的内存效率,因为加载器可以在不需要时卸载东西/写入时复制等。

          好处:

          • 模块化。如果功能区域被清晰地分开,则可以很容易地在多个开发人员之间分担工作。
          • 测试。可以单独测试一个模块(通过单元测试),而无需考虑其他代码区域。
          • 认知负荷。在编写整个应用程序时,很难保持对整个应用程序的印象。两年后,当您回到它进行升级时,情况会更加困难。想象一下,如果新开发人员加入该应用程序会是什么样子。

          结论

          • 我喜欢模块,但这取决于您的情况。如果您距离发货还有一周的时间,并且大多数事情都正常运行,并且您完全有可能修复剩下的几个小错误 - 为什么现在就改变。
          • 另一方面,如果您正在为难以解决的错误而苦苦挣扎,在更改不相关的内容时代码会神秘地中断,或者处于开发的中期阶段并落后于计划,将其分解为模块可能是一个不错的选择降低复杂性的想法。

          【讨论】:

            猜你喜欢
            • 2015-09-05
            • 1970-01-01
            • 2014-01-14
            • 1970-01-01
            • 2016-01-31
            • 2021-06-16
            • 1970-01-01
            • 2020-07-22
            • 2023-03-15
            相关资源
            最近更新 更多