在编译语言(C#/C/C++/D)中做插件的"正常"方法是什么?我对语言不可知的方法特别感兴趣,但特定语言并非不能接受。
目前,"编译时"插件方法(无论是否包含代码,一切都有效)是有效的,但可以迁移到更动态方法的东西是首选。
关于运行时类型,我对加载插件的机制更感兴趣,而不是设计插件/应用程序界面
编辑:顺便说一句,插件将是奴隶而不是主人。插件的基本操作是,在给定的情况下,它将被要求"做它的事情",并被赋予一个环境对象,它应该使用它来获取它需要运行的东西。
Mono.Addins 似乎是 .NET 的一个很好的解决方案。我相信它包含 API,允许您从存储库下载插件(或插件)并将其动态加载到正在运行的程序集中。
对于编译语言(编译意味着程序作为本机可执行文件运行,没有任何虚拟机),您几乎需要使用某种特定于平台的共享库方法。 在Windows中,这意味着使用DLL。
您可以根据一组函数(具有特定名称、参数和调用约定)来定义插件接口。 然后,在共享库中加载函数的地址并转到城镇。 在 Windows 中,这意味着使用 GetProcAddress(),然后将返回值强制转换为 C 中适当类型的函数指针,或者您正在使用的语言中的任何等效指针。
另一个可能更可取也可能更不理想的选项是从本机应用程序中运行另一种语言的虚拟机,并让插件成为该语言的代码。 例如,您可以使用CPython运行Python VM并动态加载Python模块。
我发现插件的困难部分是:找到它们,解决它们的依赖关系,以及处理版本问题。 你如何处理这些问题,你和你的插件作者都应该清楚。 如果你把这些问题弄错了,就会造成无尽的痛苦。 我会看看使用插件的脚本语言和应用程序来了解什么效果很好。
静态构造函数通常是"聪明"的。 由于无论如何,您都必须一次加载一个插件(C++在动态情况下),因此您最好在这样做时明显地初始化它们。 除此之外,这可能会让您有机会在没有预期 API 的情况下拒绝插件。
请注意,您不必对插件使用动态加载库。 也可以使用其他机制:共享内存,套接字等。
这实际上取决于你想做什么。在 Emacs 和 Gimp 中看到的常见 Unix 模式是编写一个程序,该程序由一个小的编译组件组成,该组件公开了解释组件用来执行所有操作的基本功能。提供可以在应用程序之上构建的新功能的插件很容易,但您需要在提供的基元中非常灵活才能实现这一点。在相反的极端,想象一个可以保存多种格式的照片编辑器。您希望允许用户编写自己的文件格式处理程序。这需要使代码使用一组简单的基元,然后在运行时选择一个实现。在直接(Unix)C中使用dlopen,在C++使用extern C,它限制了你可以做什么和dlopen。在Objective-C中,你有一个类可以为你做这件事。在第一种情况下,您正在制作或重复使用口译员,因此您可以自由地随心所欲地做。
对于每个插件封装一组通用函数的不同实现的从属类型插件,我只需将 DLL 存放在主机应用程序已知的插件文件夹中(例如"c:\program files\myapp\plugins),然后通过反射从主机调用 DLL。
安装每个 DLL 时,您可以执行某种复杂的注册过程,但我从未遇到过简单的插件在一个文件夹中的方法的任何问题。
编辑:要在 C# 中执行此操作,您只需将一个公共类添加到 DLL(名为"插件"或其他任何内容),并在那里实现所需的函数。然后,从主机创建一个 Plugin 类型的对象并调用其方法(全部使用 Reflection)。
带有 void 寄存器(EventSource) 的接口似乎运行良好 - 请参阅 ASP。NET的IHttpModule.Init(HttpApplication)为例。
这允许应用程序作者(控制 EventSource)根据需要添加事件,而无需扩展 IPlugin 接口(不可避免地导致 IPluginEx、IPlugin2、IPlugin2Ex 等)。
我使用的一种方法(在 .NET 中)是让主机对插件进行初始调用(通过反射),启动插件并将引用传递给插件保存的主机。 然后,该插件根据需要在主机上调用方法(也通过反射)。
我认为对于大多数插件,调用通常会在另一个方向上进行(即主机会根据需要调用插件)。 就我而言,插件本身具有需要使用主机功能的 UI 元素。
除了低级模块实现问题(例如Windows DLL和实现问题)之外,我使用的游戏引擎在DLL中只有一个全局注册函数,并尝试在插件目录中的每个dll上查找并调用它。 注册函数执行任何必要的簿记以公开功能。