关于如何实现具有类似插件架构的 c# 主机应用程序的问题

Posted

技术标签:

【中文标题】关于如何实现具有类似插件架构的 c# 主机应用程序的问题【英文标题】:Question about how to implement a c# host application with a plugin-like architecture 【发布时间】:2010-04-23 00:39:22 【问题描述】:

我想要一个可以作为许多其他小型应用程序的主机的应用程序。这些应用程序中的每一个都应该作为这个主应用程序的一种插件。我称它们为插件并不是因为它们向主应用程序添加了一些东西,而是因为它们只能与这个主机应用程序一起工作,因为它们依赖于它的一些服务。

我的想法是让这些插件中的每一个都在不同的应用程序域中运行。问题似乎是我的主机应用程序应该有一组我的插件想要使用的服务,据我了解,让数据从不同的应用程序域流入和流出并不是一件好事。

一方面,我希望它们充当独立应用程序(尽管正如我所说,它们需要多次使用宿主应用程序服务),但另一方面,如果有的话,我希望其中崩溃,我的主应用程序不会受到影响。

解决这种情况的最佳 (.NET) 方法是什么?让它们都在同一个 AppDomain 上运行,但每个都在不同的线程中?使用不同的 AppDomains?每个“插件”一个?我如何让他们与主机应用程序通信?还有其他方法吗?

虽然速度在这里不是问题,但我不希望函数调用比我们仅使用常规 .NET 应用程序时慢得多。

谢谢

编辑:也许我真的需要使用不同的 AppDomain。根据我一直在阅读的内容,在不同的 AppDomain 中加载程序集是以后能够从进程中卸载它们的唯一方法。

【问题讨论】:

【参考方案1】:

我已经使用 System.Addin 命名空间中的托管插件框架 (MAF) 实现了一些类似的东西。使用 MAF,您可以将插件打包为单独的 DLL,您的主机应用程序可以在其应用程序域、所有插件的单独域中或在其自己的域中的每个插件中发现和启动它们。使用卷影副本和单独的域,您甚至可以在不关闭主机应用程序的情况下更新插件。

您的主机应用程序和插件通过您从 MAF 接口派生的合同进行通信。您可以在主机和插件之间来回发送对象。这些协议在插件和主机之间提供了一个黑盒接口,允许您在主机不知道的情况下更改插件的实现。

如果宿主告诉它们彼此,插件甚至可以在它们之间进行通信。在我的情况下,其他人共享一个日志插件。这让我可以在不接触其他插件或主机的情况下放入不同的记录器。

对于我的应用程序,插件使用简单的主管类,它们在自己的线程上启动工作类,执行所有处理。工人捕获他们自己的异常,他们通过回调方法将其返回给他们的主管。主管可以重新启动工作人员或采取其他措施。主机通过命令合约控制监督者,指示它们启动和停止worker并返回数据。

我的主机应用程序是 Windows 服务。工作线程出于所有常见原因(包括错误!)抛出异常,但主机应用程序从未在我们的任何安装中崩溃。由于调试服务不方便,插件允许我构建使用相同合约的测试应用程序,并额外保证我正在测试我部署的内容。

插件也可以暴露 UI 元素。这对我很有帮助,因为我需要使用主机服务部署控制器应用程序,因为服务没有 UI。每个插件都包含自己的控制器接口。控制器应用程序本身非常简单 - 它加载插件并显示它们的 UI 元素。这使我可以使用更新的界面发布更新的插件,而不必发布新的控制器。

即使控制器和主机服务使用相同的插件,它们也不会相互踩踏;事实上,他们甚至不知道另一个应用程序正在使用相同的插件。控制器和主机通过共享数据库相互通信,但您也可以使用另一种应用程序间机制,如 MSMQ。在下一个版本中,主机将是一个 WCF 服务,在后端带有插件和用于控制的 Web 服务。

这有点啰嗦,但我想让您了解 MAF 的多功能性。它并不像最初看起来那么复杂,您可以使用它构建坚如磐石的应用程序。

【讨论】:

【参考方案2】:

这取决于您希望允许扩展的信任程度。我正在开发一个类似的应用程序,并且我选择主要信任扩展代码,因为这大大简化了事情。我从一个公共线程调用代码(在我的例子中,扩展并没有真正在任何连续循环中“运行”,而是执行主应用程序想要执行的某些任务)并在这个线程中捕获异常,例如提供有用的警告,说明加载的扩展程序行为不端。

目前,没有什么可以阻止这些扩展程序启动它们自己的线程,这可能会引发整个应用程序的崩溃,但我不得不在安全性和复杂性之间做出权衡。我的应用程序不是关键任务(不像 Web 服务器或数据库服务器),所以我认为有缺陷的扩展可能会导致我的应用程序崩溃是可以接受的风险。我提供保护措施以更礼貌地涵盖最常见的失败案例,并将其留给插件开发人员(无论如何,他们现在主要是内部人员)来清理他们的错误。


关于卸载,是的,如果您将程序集放在 AppDomain 中,您只能卸载程序集的代码和元数据。也就是说,除非您希望在程序的整个生命周期中频繁地加载和卸载,否则将代码保存在内存中的相关开销不一定是问题。当您停止“使用”它时,使用程序集类型的任何实际实例或资源仍将被 GC 清理,因此它仍在内存中的事实并不意味着内存泄漏。

如果您的主要用例是一系列插件,您在启动时定位一次,然后在您的应用程序运行时提供实例化选项,我建议调查与在启动时加载所有这些插件相关的实际内存占用,并保持他们加载。如果您使用 AppDomains,那里也会有额外的开销(例如,代理对象的内存和加载/JITed 代码以支持 AppDomain 编组)。还存在与封送处理和伴随的序列化相关的 CPU 开销。

简而言之,如果满足以下条件之一,我只会使用 AppDomain:

出于代码安全的目的,我想获得真正的隔离(即我需要以隔离的方式运行不受信任的代码)

我的应用程序是关键任务,我绝对需要确保如果插件失败,它不会导致我的核心应用程序崩溃。

我需要反复加载和卸载同一个插件,以支持动态更改 DLL。这主要是如果我的应用程序无法停止运行,但我想在它仍在运行时对插件进行热补丁。

我不希望 AppDomain 仅用于通过允许卸载来减少可能的内存占用。

【讨论】:

这是一个爱好应用,没什么特别的。我完全相信正在运行的代码。问题是我需要我的每个插件一直运行(即我不能让我的主机应用程序调用我的插件,它们必须是独立的并且自己运行。) 在这种情况下,我只需让每个线程运行并调用您定义的任何“主”方法。如果您想在插件抛出异常的情况下提供友好的回退,您可以选择将线程对“Main”的调用包装在 try/catch 中。 是的,但请参阅上面的编辑。这样我以后就无法在不重新启动应用程序的情况下卸载加载的程序集。我必须将它们放在不同的应用程序域中,以便我可以随时卸载它们。 我想至少在我第一次尝试插件时加载和卸载它们。每次我想重新编译插件 X 或 Y 时,我都不想杀死我的主应用程序。:( 您实际上可以并行加载插件(即导入程序集的全新副本),这当然会在测试期间积累内存,但这是一种选择。基本上,您释放对旧插件的任何引用并使用来自同一程序集的新 Load 的类型。您只需要在此处小心释放所有旧引用,以避免出现奇怪的异常,例如 ('Object' (of type 'Type') is not of type 'Type')【参考方案3】:

这是一个有趣的问题。

我的第一个想法是在插件应用程序中简单地实现来自主机应用程序的接口,以允许它们通过反射进行通信,但这只会允许通信并且不会带来真正的“类似沙盒”的架构。

我的第二个想法是设计一个面向服务的平台。主机应用程序将是一种“插件广播器”,它将在不同线程上的 ServiceHost 中发布您的插件。由于这需要真正响应并且“无需考虑配置”,主机应用程序可以通过命名管道通道(WCF 的 NetNamedPipesBinding)与插件通信,这意味着只与 localhost 管道通信,根本不需要任何网络配置或知识.我认为这可以很好地解决您的问题。

问候。

【讨论】:

以上是关于关于如何实现具有类似插件架构的 c# 主机应用程序的问题的主要内容,如果未能解决你的问题,请参考以下文章

在C#程序中实现插件架构

如何在 Flutter Dart 中实现插件架构

如何使用 Windows 运行时使用 C# 实现 C++ API?

如何在 C# 中实现 3 层架构

C# 关于Struct的思考

VST 主机 - MIDI 到波形转换 (C#)