- 7.1 What Is a Plugin?
- 7.2 Problem 1: The Plugin Interface 问题1:插件接口
- 7.3 Problem 2: Loading Plugins 问题2:加载插件
- 7.4 Problem 3: Retrofitting pdCalc 问题3:改造pdCalc
- 7.5 Incorporating Plugins 融入插件
- 7.6 A Concrete Plugin 具体插件
- 7.7 Next Steps
You’ve probably read the chapter title, so you already know that this chapter is about plugins, specifically their design and implementation. Additionally, plugins will afford us the opportunity to explore design techniques to isolate platform-specific features. Before we dive into the details, however, let’s begin by defining what a plugin is.
你可能已经读过本章的标题,所以你已经知道本章是关于插件的,特别是它们的设计和实现。此外,插件将使我们有机会探索设计技术,以隔离平台的特定功能。然而,在我们深入研究这些细节之前,让我们先来定义一下什么是插件。
7.1 What Is a Plugin?
A plugin is a software component that enables new functionality to be added to a program after the program’s initial compile. In this chapter, we’ll concentrate exclusively on runtime plugins, that is, plugins built as shared libraries (e.g., a POSIX .so or Windows .dll file) that are discoverable and loadable at runtime.
插件是一种软件组件,可以在程序初始编译后向程序添加新功能。在这一章中,我们将专注于运行时插件,也就是构建为共享库(例如POSIX .so或Windows .dll文件)的插件,它们可以在运行时被发现和加载。
Plugins are useful in applications for a myriad of different reasons. Here are just a few examples. First, plugins are useful for allowing end users to add features to an existing program without the need to recompile. Often, these are new features that were completely unanticipated by the original application developers. Second, architecturally, plugins enable separation of a program into multiple optional pieces that can be individually shipped with a program. For example, consider a program (e.g., a web browser) that ships with some base functionality but allows users to add specialty features (e.g., an ad blocker). Third, plugins can be used for designing an application that can be customized to a specific client. For example, consider an electronic health records system that needs different functionality depending on whether the software is deployed at a hospital or a physician’s individual practice. The necessary customizations could be captured by different modules that plug into a core system. Certainly, one can think of many additional applications for plugins.
插件在应用程序中有许多不同的用途。这里有几个例子。首先,插件可用于允许最终用户将功能添加到现有程序,而不需要重新编译。通常,这些新特性是原始应用程序开发人员完全没有预料到的。其次,从架构上讲,插件允许将程序分离为多个可选的片段,这些片段可以单独与程序一起提供。例如,考虑一个程序(如网页浏览器),它提供一些基本功能,但允许用户添加一些特殊功能(如广告拦截器)。第三,插件可以用于设计应用程序,可以根据特定的客户端进行定制。例如,考虑一个电子健康记录系统,它需要不同的功能,这取决于该软件是部署在医院还是部署在医生的个人实践中。必要的定制可以由插入核心系统的不同模块捕获。当然,人们可以想到许多附加的插件应用程序。
In the context of pdCalc, plugins are shared libraries that provide new calculator commands, and, optionally, new GUI buttons. How difficult could that task be? In Chapter 4, we created numerous commands and saw that adding new ones was fairly trivial.
在pdCalc上下文中,插件是提供新的计算器命令和(可选的)新的GUI按钮的共享库。这个任务能有多难呢?在第4章中,我们创建了许多命令,并发现添加新命令相当简单。
We simply inherited from the Command class (or one of its derived classes, such as UnaryCommand or BinaryCommand), instantiated the command, and registered it with the CommandRepository. For example, take the sine command, which is declared in CoreCommands.h as follows:
我们只是简单地继承了Command类(或者它的一个派生类,如UnaryCommand或BinaryCommand),实例化了命令,并在CommandRepository中注册了它。以sin命令为例,它在CoreCommands.h中声明如下:
class Sine : public UnaryCommand
{
// implement Command virtual members
};
and registered in CoreCommands.cpp by the line
注册在CoreCommands.cpp中
registerCommand( ui, "sin", MakeCommandPtr<Sine>() );
It turns out that this recipe can be followed almost exactly by a plugin command except for one crucial step. Since the plugin command’s class name is unknown to pdCalc at compile time, we cannot use the plugin class’s name for allocation.
事实证明,除了一个关键步骤之外,这个方法几乎可以完全遵循一个插件命令。由于pdCalc在编译时不知道plugin命令的类名,所以我们不能使用插件类名进行分配。
This seemingly simple dilemma of not knowing the class names of plugin commands leads to the first problem we’ll need to solve for plugins. Specifically, we’ll need to establish an abstract interface by which plugin commands become discoverable to and registered within pdCalc. Once we’ve agreed upon a plugin interface, we’ll quickly encounter the second fundamental plugin problem, which is how do you dynamically load a plugin to even make the names in the shared library available to pdCalc. To make our lives more complicated, the solution to this second problem is platform dependent, so we’ll seek a design strategy that minimizes the platform dependency pain. The final problem we’ll encounter is updating our existing code to add new commands and buttons dynamically. Maybe surprisingly, this last problem is the easiest to solve. Before we get started tackling our three problems, however, we need to consider a few rules for C++ plugins.
这个看似简单的难题是不知道插件命令的类名,这导致了我们需要解决的插件的第一个问题。具体来说,我们需要建立一个抽象接口,通过这个接口,插件命令可以在pdCalc中被发现和注册。一旦我们对插件接口达成一致,我们很快就会遇到第二个基本的插件问题,即如何动态加载插件,甚至使共享库中的名称对pdCalc可用。为了使我们的生活更加复杂,第二个问题的解决方案是依赖于平台的,因此我们将寻求一种设计策略,将平台依赖的痛苦最小化。我们将遇到的最后一个问题是更新现有代码来动态添加新的命令和按钮。也许令人惊讶的是,最后一个问题是最容易解决的。然而,在开始解决这三个问题之前,我们需要考虑c++插件的一些规则。
7.1.1 Rules for C++ Plugins C++插件规则
Plugins are not conceptually part of the C++ language. Rather, plugins are a manifestation of how the operating system dynamically loads and links shared libraries (hence the platform-specific nature of plugins). For any nontrivially sized project, the application is typically divided into an executable and several shared libraries (traditionally, .so files in Unix, .dylib files in Mac OS X, and .dll files in MS Windows).
插件在概念上不是c++语言的一部分。相反,插件是操作系统如何动态加载和链接共享库的一种表现(因此插件具有特定于平台的特性)。对于任何规模较大的项目,应用程序通常被划分为一个可执行文件和几个共享库(传统上,Unix中的.so文件、Mac OS X中的.dylib文件和MS Windows中的.dll文件)。
Ordinarily, as C++ programmers, we remain blissfully unaware of the subtleties this structure entails because the executable and libraries are built in a homogeneous build environment (i.e., same compiler and standard libraries). For a practical plugin interface, however, we have no such guarantee. Instead, we must program defensively and assume the worst case scenario, which is that plugins are built in a different, but compatible, environment to the main application. Here, we’ll make the relatively weak assumption that the two environments, at minimum, share the same object model. Specifically, we require that the two environments use the same layout for handling the virtual function pointer (vptr). If you are unfamiliar with the concept of a virtual function pointer, all the gory details can be found in Lippman [13]. While in principle, C++ compiler writers may choose different vptr layouts, in practice, compilers typically use compatible layouts, especially different versions of the same compiler. Without this shared object model assumption, we would be forced to develop a C language-only plugin structure. Note that we must also assume that sizeof(T) is the same size for all types T in the main application and plugins. This eliminates, for example, having a 32-bit application and a 64-bit plugin because these two platforms have different pointer sizes.
作为c++程序员,我们很幸运地没有意识到这种结构的微妙之处,因为可执行文件和库都是在同构的构建环境中构建的。相同的编译器和标准库)。然而,对于一个实用的插件接口,我们没有这样的保证。相反,我们必须进行防御性的编程,并假设最坏的情况,即插件是在与主应用程序不同但兼容的环境中构建的。这里,我们将做一个相对较弱的假设,即这两个环境至少共享相同的对象模型。特别地,我们要求这两个环境使用相同的布局来处理虚拟函数指针(vptr)。您不熟悉虚拟函数指针的概念,所有血淋淋的细节都可以在Lippman[13]中找到。虽然在原则上,c++编译器编写者可以选择不同的vptr布局,但在实践中,编译器通常使用兼容的布局,特别是同一编译器的不同版本。如果没有这个共享对象模型的假设,我们将被迫开发一个只支持C语言的插件结构。注意,我们还必须假设,对于主应用程序和插件中的所有类型T, sizeof(T)的大小都是相同的。例如,这消除了32位应用程序和64位插件,因为这两个平台有不同的指针大小。
How does programming in a heterogeneous environment affect the available programming techniques we can use? In the worst case scenario, the main application might be built with both a different compiler and a different standard library. This fact has several serious implications. First, we cannot assume that allocation and deallocation of memory between plugins and the application are compatible. This means any memory new-ed in a plugin must be delete-ed in the same plugin. Second, we cannot assume that code from the standard library is compatible between any plugin and the main application. Therefore, our plugin interface cannot contain any standard containers. While library incompatibility might seem odd (it’s the standard library, right?), remember that the standard specifies the interface, not the implementation (subject to some restrictions, such as vectors occupying contiguous memory). For example, different standard library implementations frequently have different string implementations. Some prefer the small string optimization while others prefer using copy-on-write. Third, while we have assumed a compatible layout for the vptr in our objects, we cannot assume identical alignment. Therefore, plugin classes should not inherit from main application classes that have member variables defined in the base classes if these member variables are used in the main application. This follows since the main application’s compiler may use a different memory offset to a member variable than what was defined by the plugin’s compiler if each compiler uses different alignment. Fourth, due to name mangling variances across different compilers, exported interfaces must specify extern “C” linkage. The linkage requirement is bidirectional. Plugins should not call application functions without extern “C” linkage, nor should the application call plugin functions without extern “C” linkage. Note that because non-inline, non-virtual member functions require linkage across compilation units (as opposed to virtual functions, which are called via the vptr through an offset in the virtual function table), the application should only call into plugin code through virtual functions, and plugin code should not call base class non-inline, non-virtual functions compiled in the main application. Finally, exceptions are rarely portable across the binary interface between the main program and plugins, so we cannot throw exceptions in a plugin and try to catch them in the main application.
在异构环境中编程如何影响我们可以使用的可用编程技术?在最坏的情况下,主应用程序可能使用不同的编译器和不同的标准库构建。这一事实有几个严重的影响。首先,我们不能假设插件和应用程序之间的内存分配和回收是兼容的。这意味着任何在插件中新增的内存都必须在同一个插件中删除。其次,我们不能假设标准库中的代码在任何插件和主应用程序之间都是兼容的。因此,我们的插件接口不能包含任何标准容器。虽然库的不兼容性看起来很奇怪(它是标准库,对吧?),但请记住,标准指定的是接口,而不是实现(受到一些限制,例如占用连续内存的向量)。例如,不同的标准库实现通常有不同的字符串实现。有些人喜欢使用小字符串优化,而另一些人喜欢使用写时复制。第三,虽然我们已经为对象中的vptr假设了兼容的布局,但我们不能假设相同的对齐方式。因此,如果在主应用程序中使用了基类中定义的成员变量,那么插件类不应该继承主应用程序类。这是因为如果主应用程序的编译器使用不同的对齐方式,那么主应用程序的编译器对成员变量使用的内存偏移量可能与插件编译器定义的不同。第四,由于不同编译器之间的名称混乱差异,导出的接口必须指定extern“C”链接。链接的要求是双向的。插件不应该调用没有外部C链接的应用程序函数,应用程序也不应该调用没有外部C链接的插件函数。注意,由于非内联,非虚拟成员函数需要链接跨编译单元(而不是虚函数,称为通过vptr通过虚函数表中一个偏移量),应用程序应该只通过虚拟函数,调用插件代码和插件代码不应调用基类非内联,在主应用程序中编译的非虚函数。最后,异常很难在主程序和插件之间的二进制接口上移植,所以我们不能在插件中抛出异常,并试图在主应用程序中捕获它们。
That was a mouthful. Let’s recap by enumerating the rules for C++ plugins:
- Memory allocated in a plugin must be deallocated in the same plugin.
- Standard library components cannot be used in plugin interfaces.
- Assume incompatible alignment. Avoid plugins inheriting from main application classes with member variables if the variables are used in the main application.
- Functions exported from a plugin (to be called by the main application) must specify extern “C” linkage. Functions exported from the main application (to be called by plugins) must specify extern “C” linkage.
- The main application should communicate with plugin-derived classes exclusively via virtual functions.Plugin-derived classes should not call non-inline, non-virtual main application base class functions.
- Do not let exceptions thrown in plugins propagate to the main application.
真够拗口的。让我们回顾一下c++插件的规则:
- 在一个插件中分配的内存必须在同一个插件中回收。
- 标准库组件不能在插件接口中使用。
- 假设不兼容的对齐。避免插件继承主应用程序类的成员变量,如果这些变量在主应用程序中使用。
- 从插件导出的函数(由主应用程序调用)必须指定extern “C”链接。从主应用程序导出的函数(由插件调用)必须指定extern “C”链接。
- 主应用程序应该只通过虚函数与插件派生类通信。插件派生类不应该调用非内联的、非虚拟的主应用程序基类函数。
- 不要让插件中抛出的异常传播到主应用程序。
With these rules in mind, let’s return to the three fundamental problems we must solve in order to design plugins.
记住这些规则,让我们回到设计插件必须解决的三个基本问题。
7.2 Problem 1: The Plugin Interface 问题1:插件接口
The plugin interface is responsible for several items. First, it must enable the discovery of both new commands and new GUI buttons. We’ll see that this functionality is most effectively accomplished through a class interface. Second, the plugin must support a C linkage interface for allocating and deallocating the aforementioned plugin class. Third, pdCalc should provide a PluginCommand class derived from Command to assist in correctly writing plugin commands. Technically, a PluginCommand class is optional, but providing such an interface helps users conform to Plugin Rules 3 and 6. Fourth, it is worthwhile for the plugin interface to provide a function for querying the API version that a plugin supports. Finally, pdCalc must provide C linkage for any of the functions it must make available for plugins to call. Specifically, plugin commands must be able to access the stack. We’ll address these issues in sequence starting with the interface for discovering commands.
插件接口负责几个项目。首先,它必须能够发现新的命令和新的GUI按钮。我们将看到这个功能是通过类接口最有效地实现的。其次,插件必须支持一个C链接接口来分配和释放上述插件类。第三,pdCalc应该提供一个从Command派生的PluginCommand类来帮助正确编写插件命令。从技术上讲,PluginCommand类是可选的,但是提供这样的接口可以帮助用户遵守插件规则3和6。第四,插件接口提供一个查询插件支持的API版本的函数是值得的。最后,pdCalc必须为它必须为插件调用提供的任何函数提供C链接。特别地,插件命令必须能够访问堆栈。我们将从发现命令的接口开始依次解决这些问题。
7.2.1 The Interface for Discovering Commands 用于发现命令的接口
The first problem we face is how to allocate commands from plugins when we know neither what commands the plugin provides nor the names of the classes we’ll need to instantiate. We’ll solve this problem by creating an abstract interface to which all plugins must conform that exports both commands and their names. First, let’s address what functionality we’ll need.
我们面临的第一个问题是,当我们既不知道插件提供了什么命令,也不知道需要实例化的类的名称时,如何从插件中分配命令。我们将通过创建一个抽象接口来解决这个问题,所有插件都必须遵循这个抽象接口来导出命令和它们的名称。首先,让我们看看我们需要什么功能。
Recall from Chapter 4, that in order to load a new command into the calculator, we must register it with the CommandRepository. By design, the CommandRepository was specifically constructed to admit dynamic allocation of commands, precisely the functionality we need for plugin commands. For now, we’ll assume that the plugin management system has access to the register command (we’ll address this deficiency in Section 7.4). The CommandRepository’s registration function requires a string name for the command and a unique_ptr that serves as a prototype for the command. Since pdCalc knows nothing a priori about the command names in a plugin, the plugin interface must first make the names discoverable. Second, since C++ lacks reflection as a language feature, the plugin interface must provide a way to create a prototype command to be associated with each of the discovered names. Again, by design, the abstract Command interface supports the prototype pattern via the clone() virtual member function. Let’s see how these two prior design decisions effectively enable plugins.
回想第4章,为了将一个新命令加载到计算器中,我们必须将它注册到CommandRepository。通过设计,CommandRepository被特别构造为允许动态分配命令,这正是我们需要的插件命令的功能。现在,我们假设插件管理系统可以访问register命令(我们将在7.4节中解决这个缺陷)。CommandRepository的注册函数需要命令的字符串名称和作为命令原型的unique_ptr。由于pdCalc对插件中的命令名一无所知,因此插件接口必须首先使这些命令名可被发现。除此之外,由于c++没有作为语言特性的反射,插件接口必须提供一种方法来创建与每个发现的名称相关联的原型命令。同样,通过设计,抽象的Command接口通过clone()虚拟成员函数支持原型模式。让我们看看这两个先前的设计决策是如何有效地启用插件的。
Based on the C++ plugin rules above, the only means we have to effect command discovery is to encapsulate it as a pure virtual interface to which all plugins must adhere. Ideally, our virtual function would return an associative container of unique ptr
基于以上的c++插件规则,我们要实现命令发现的唯一方法就是将它封装为一个所有插件都必须遵守的纯虚拟接口。理想情况下,我们的虚函数将返回一个关联容器,该容器包含由字符串键组成的unique
The above design is enforced by creating a Plugin class to which all plugins must conform. The purpose of this abstract class is to standardize plugin command discovery. The class declaration is given by the following:
上述设计是通过创建一个所有插件都必须遵循的Plugin类来实现的。这个抽象类的目的是标准化插件命令发现。类的声明如下所示:
class Plugin {
public:
Plugin();
virtual ~Plugin();
struct PluginDescriptor {
int nCommands;
char** commandNames;
Command** commands;
};
virtual const PluginDescriptor& getPluginDescriptor() const = 0;
};
We now have an abstract plugin interface that, when specialized, requires a derived class to return a descriptor that provides the number of commands available, the names of these commands, and prototypes of the commands themselves. Obviously, the ordering of the command names must match the ordering of the command prototypes. Unfortunately, with raw pointers and raw arrays, the ambiguity of who owns the memory for the command names and command prototypes arises. Our inability to use standard containers forces us into an unfortunate design: contract via comment. Since our rules dictate that memory allocated in a plugin must be freed by the same plugin, the best strategy is to decree that plugins are responsible for deallocation of the PluginDescriptor and its constituents. As stated before, the memory contract is “enforced” by comment.
现在我们有了一个抽象的插件接口,它需要一个派生类来返回一个描述符,该描述符提供可用命令的数量、这些命令的名称以及命令本身的原型。显然,命令名的顺序必须与命令原型的顺序相匹配。不幸的是,对于原始指针和原始数组,谁拥有命令名和命令原型的内存会产生歧义。我们无法使用标准容器迫使我们陷入了一个不幸的设计:通过评论契约。由于我们的规则规定在一个插件中分配的内存必须由同一个插件释放,最好的策略是规定插件负责释放PluginDescriptor及其组件。如前所述,内存契约是通过注释“强制”的。
Great, our problem is solved. We create a plugin, let’s call it MyPlugin, that inherits from Plugin. We’ll see how to allocate and deallocate the plugin in Section 7.3 below. Inside of MyPlugin, we create new commands by inheriting from Command as usual. Since the plugin knows its own command names, unlike the main program, the plugin can allocate its command prototypes with the new operator. Then, in order to register all the plugin’s commands, we simply allocate a plugin descriptor with both command names and command prototypes, return the descriptor by overriding the getPluginDescriptor() function, and let pdCalc register the commands. Since Commands must each implement a clone() function, pdCalc can copy the plugin command prototypes via this virtual function to register them with the CommandRepository. Trivially, string names for registration can be created from the commandNames array. For an already allocated Plugin p, the following code within pdCalc could implement registration:
太好了,我们的问题解决了。我们创建一个插件,我们称之为MyPlugin,它继承自plugin。我们将在下面的7.3节中看到如何分配和释放插件。在MyPlugin内部,我们像往常一样通过继承Command来创建新的命令。因为插件知道自己的命令名,不像主程序,插件可以用new操作符分配它的命令原型。然后,为了注册所有插件的命令,我们简单地分配一个包含命令名和命令原型的插件描述符,通过覆盖getPluginDescriptor()函数返回描述符,让pdCalc注册这些命令。由于Commands必须每个实现一个clone()函数,pdCalc可以通过这个虚拟函数复制插件命令原型,并将它们注册到CommandRepository。简单地说,可以从commandNames数组创建用于注册的字符串名称。对于一个已经分配的插件 p, pdCalc中的以下代码可以实现注册:
const Plugin::PluginDescriptor& d = p->getPluginDesciptor();
for (int i = 0; i < d.nCommands; ++i)
CommandRepository::Instance().registerCommand(d.commandNames[i],
MakeCommandPtr(d.commands[i]->clone()));
At this point, you might recognize a dilemma with our plugins. Commands are allocated in a plugin, copied upon registration with the CommandRepository in the main program via a clone() call, and then ultimately deleted by the main program when CommandRepository’s destructor executes. Even worse, every time a command is executed, the CommandRepository clones its prototype, triggering a new statement in the plugin via Command’s clone() function. The lifetime of this executed command is managed by the CommandManager through its undo and redo stacks. Specifically, when a command is cleared from one of these stacks, delete is called in the main program when the unique_ptr holding the command is destroyed. At least, that’s how it works without some tweaking. As was alluded to in Chapter 4, CommandPtr is more than a simple alias for unique_ptr
在这一点上,您可能会意识到我们的插件的一个困境。命令在插件中分配,在主程序中通过clone()调用注册到CommandRepository时复制,然后在CommandRepository的析构函数执行时最终被主程序删除。更糟糕的是,每次执行命令时,CommandRepository都会克隆它的原型,通过command的clone()函数在插件中触发一条新语句。这个被执行命令的生命周期由CommandManager通过它的撤销和重做堆栈来管理。具体地说,当从这些堆栈中清除一个命令时,当保存该命令的unique_ptr被销毁时,将在主程序中调用delete。至少,这是它的工作原理,没有一些调整。正如在第4章中提到的,CommandPtr不仅仅是unique_ptr
Fundamentally, we first need a function to call delete in the appropriate compilation unit. The easiest solution to this problem is to add a deallocate() virtual function to the Command class. The responsibility of this function is to invoke delete in the correct compilation unit when Commands are destroyed. For all core commands, the correct behavior is simply to delete the class in the main program. Hence, we do not make the deallocate() function pure virtual, and we give it the following default implementation:
基本上,我们首先需要一个在适当的编译单元中调用delete的函数。这个问题最简单的解决方案是在Command类中添加一个deallocate()虚拟函数。这个函数的职责是在命令被销毁时在正确的编译单元中调用delete。对于所有核心命令,正确的行为是简单地删除主程序中的类。因此,我们没有将deallocate()函数设置为纯虚函数,而是给了它以下默认实现:
void Command::deallocate()
{
delete this;
}
For plugin commands, the override for the deallocate() has the same definition, only the definition appears in the plugin’s compiled code (say, in a base class used by commands in a particular plugin). Therefore, when deallocate() is invoked on a Command pointer in the main application, the virtual function dispatch ensures that delete is called from the correct compilation unit. Now we just need a mechanism to ensure that we call deallocate() instead of directly calling delete when Commands are reclaimed. Fortunately, it’s as if the standards committee anticipated our needs perfectly when they designed unique_ptr. Let’s return to the CommandPtr alias to see how unique_ptr can be used to solve our problem.
对于插件命令,deallocate() 的覆盖具有相同的定义,只有定义出现在插件的编译代码中(例如,在特定插件中的命令使用的基类中)。 因此,当在主应用程序中的命令指针上调用 deallocate() 时,虚函数调度确保从正确的编译单元调用 delete。 现在我们只需要一种机制来确保我们在回收命令时调用 deallocate() 而不是直接调用 delete 。 幸运的是,就好像标准委员会在设计 unique_ptr 时完美地预料到了我们的需求。 让我们回到 CommandPtr 别名来看看如何使用 unique_ptr 来解决我们的问题。
Remarkably few lines of code are necessary to define a CommandPtr alias and to implement a MakeCommandPtr() function capable of invoking deallocate() instead of delete. The code makes use of unique_ptr’s deleter object (see sidebar), which enables a custom routine to be called to reclaim the resource held by the unique_ptr when the unique_ptr’s destructor is invoked. Let’s look at the code:
值得注意的是,定义 CommandPtr 别名和实现能够调用 deallocate() 而不是 delete 的 MakeCommandPtr() 函数需要几行代码。 该代码使用了 unique_ptr 的删除器对象(参见边栏),当调用 unique_ptr 的析构函数时,该对象允许调用自定义例程来回收 unique_ptr 持有的资源。 让我们看一下代码:
inline void CommandDeleter(Command* p)
{
p->deallocate();
return;
}
using CommandPtr = unique_ptr<Command, decltype(&CommandDeleter)>;
inline auto MakeCommandPtr(Command* p)
{
return CommandPtr { p, &CommandDeleter };
}
A brief explanation of the dense code above is warranted. A CommandPtr is simply an alias for a unique_ptr that contains a Command pointer that is reclaimed by calling the CommandDeleter() function at destruction. The CommandDeleter() function invoked by the unique_ptr is a simple inline function that calls the virtual deallocate() function previously defined. To ease the syntactic burden of creating CommandPtrs, we introduce an inlined MakeCommandPtr() helper function that constructs a CommandPtr from a Command pointer. That’s it. Now, just as before, unique_ptrs automatically manage the memory for Commands. However, instead of directly calling delete on the underlying Command, the unique_ptr’s destructor invokes the CommandDeleter function, which calls deallocate(), which issues a delete on the underlying Command in the correct compilation unit.
有必要对上面的密集代码进行简要说明。 CommandPtr 只是 unique_ptr 的别名,它包含一个 Command 指针,该指针通过在销毁时调用 CommandDeleter() 函数来回收。 unique_ptr 调用的 CommandDeleter() 函数是一个简单的内联函数,它调用先前定义的虚拟 deallocate() 函数。 为了减轻创建 CommandPtr 的语法负担,我们引入了一个内联的 MakeCommandPtr() 辅助函数,它从一个 Command 指针构造一个 CommandPtr。 就是这样。 现在,就像以前一样,unique_ptrs 自动管理命令的内存。 但是,unique_ptr 的析构函数不是直接调用底层 Command 上的 delete,而是调用 CommandDeleter 函数,该函数调用 deallocate(),在正确的编译单元中对底层 Command 发出删除。
If you look at the source code for MakeCommandPtr(), in addition to the version of the function seen above that takes a Command pointer argument, you will see a very different overload that uses a variadic template and perfect forwarding. This overloaded function must exist due to a different semantic usage of MakeCommandPtr() in the construction of stored procedures. We’ll revisit the reasoning behind the two forms of the function in Chapter 8. If the suspense is overwhelming, feel free to skip ahead to Section 8.1.2.
如果您查看 MakeCommandPtr() 的源代码,除了上面看到的采用命令指针参数的函数版本之外,您还会看到使用可变参数模板和完美转发的非常不同的重载。 由于 MakeCommandPtr() 在存储过程构造中的不同语义用法,此重载函数必须存在。 我们将在第 8 章中重新审视函数的两种形式背后的推理。如果悬念太多,请随意跳到第 8.1.2 节。
MODERN C++ DESIGN NOTE: UNIQUE_PTR DESTRUCTION SEMANTICS
现代 C++ 设计注意:UNIQUE_PTR 破坏语义
The unique_ptr
unique_ptr
template <typename T, typename D = default_delete<T>>
class unique_ptr {
T* p_;
D d_;
public:
~unique_ptr()
{
d_(p_);
}
};
153template <typename T, typename D = default_delete<T>>
class unique_ptr {
T* p_;
D d_;
public:
~unique_ptr()
{
d_(p_);
}
};
Rather than directly calling delete, unique_ptr’s destructor passes the owned pointer to the deleter using function call semantics. Conceptually, default_delete is implemented as follows:
unique_ptr 的析构函数不是直接调用 delete,而是使用函数调用语义将拥有的指针传递给删除器。 从概念上讲,default_delete 实现如下:
template <typename T>
struct default_delete {
void operator()(T* p)
{
delete p;
}
};
That is, the defaultdelete simply deletes the underlying pointer contained by the unique_ptr. However, by specifying a custom deleter callable object during construction (the D template argument), unique_ptr can be used to free resources requiring customized deallocation semantics. As a trivial example, combined with a lambda expression, unique ptr’s delete semantics allow us to create a simple RAII (resource acquisition is initialization) container class, MyObj, allocated by malloc():
也就是说,default_delete 只是删除由 unique_ptr 包含的底层指针。 但是,通过在构造期间指定自定义删除器可调用对象(D 模板参数),unique_ptr 可用于释放需要自定义释放语义的资源。作为一个简单的例子,结合一个 lambda 表达式,unique_ptr 的删除语义允许我们创建一个简单的 RAII(资源获取即初始化)容器类 MyObj,由 malloc() 分配:
MyObj* m = static_cast<MyObj*>(malloc(sizeof(MyObj)));
auto d = [](MyObj* p) { free(p); };
auto p = unique_ptr<MyObj, decltype(d)> { m, d };
Of course, our design for pdCalc shows another instance of the usefulness of the custom delete semantics of unique_ptr. It should be noted that shared_ptr also accepts a custom deleter in an analogous fashion.
当然,我们对 pdCalc 的设计展示了 unique_ptr 的自定义删除语义的有用性的另一个实例。 应该注意的是,shared_ptr 也以类似的方式接受自定义删除器。
7.2.2 The Interface for Adding New GUI Buttons 添加新GUI按钮的界面
Conceptually, dynamically adding buttons is not much different than dynamically adding commands. The main application does not know what buttons need to be imported from the plugin, so the Plugin interface must provide a virtual function providing a button descriptor. Unlike commands, however, the plugin does not actually need to allocate the button itself. Recall from Chapter 6 that the GUI CommandButton widget only requires text for construction. In particular, it needs the push button’s display text (optionally, the shifted state text) and the command text issued with the clicked() signal. Therefore, even for plugin commands, the corresponding GUI button itself resides entirely in the main application; the plugin must only provide text. This leads to the following trivial interface in the Plugin class:
从概念上讲,动态添加按钮与动态添加命令没有什么不同。主程序不知道哪些按钮需要从插件中导入,所以插件接口必须提供一个提供按钮描述符的虚拟函数。然而,与命令不同,插件实际上不需要分配按钮本身。回顾一下第6章,GUI CommandButton部件的构造只需要文本。特别是,它需要按钮的显示文本(可选的是移位的状态文本)和用clicked()信号发出的命令文本。因此,即使是插件的命令,相应的GUI按钮本身也完全驻留在主程序中;插件必须只提供文本。这导致了在插件类中出现了以下微不足道的接口。
class Plugin {
public:
struct PluginButtonDescriptor {
int nButtons;
char** dispPrimaryCmd; // primary command label
char** primaryCmd; // primary command
char** dispShftCmd; // shifted command label
char** shftCmd; // shifted command
};
virtual const PluginButtonDescriptor* getPluginButtonDescriptor() const = 0;
};
Again, due to the rules we must follow for plugins, the interface must be comprised of low-level arrays of characters rather than a higher level STL construct.
同样,由于我们必须遵循插件的规则,接口必须由低级别的字符数组组成,而不是高级别的STL结构。
One interesting facet of the getPluginButtonDescriptor() function relative to the getPluginDescriptor() is the decision to return a pointer rather than a reference. The rationale behind this choice is that a plugin writer might wish to write a plugin that exports commands that do not have corresponding GUI buttons (i.e., CLI-only commands). The converse, of course, is nonsensical. That is, I cannot envision why someone would write a plugin that exported buttons for nonexistent commands. This practicality is captured in the return type for the two descriptor functions. Since both functions are pure virtual, Plugin specializations must implement them. Because getPluginDescriptor() returns a reference, it must export a non-null descriptor. However, by returning a pointer to the descriptor, getPluginButtonDescriptor() is permitted to return a nullptr indicating that the plugin exports no buttons. One might argue that the getPluginButtonDescriptor() function should not be pure virtual and instead provide a default implementation that returns a nullptr. This decision is technically viable. However, by insisting a plugin author manually implement getPluginButtonDescriptor(), the interface forces the decision to be made explicitly.
相对于getPluginDescriptor(),getPluginButtonDescriptor()函数的一个有趣的方面是决定返回一个指针而不是一个引用。这个选择背后的理由是,一个插件编写者可能希望编写一个导出没有相应GUI按钮的命令的插件(即,仅有CLI命令)。当然,反过来说也是无稽之谈。也就是说,我无法想象为什么有人会写一个为不存在的命令导出按钮的插件。这种实用性在这两个描述符函数的返回类型中得到了体现。因为这两个函数都是纯虚拟的,所以插件的专业化必须实现它们。因为getPluginDescriptor()返回一个引用,它必须导出一个非空的描述符。然而,通过返回描述符的指针,getPluginButtonDescriptor()被允许返回一个nullptr,表示该插件没有输出按钮。有人可能会说,getPluginButtonDescriptor()函数不应该是纯虚拟的,而是提供一个默认的实现,返回一个nullptr。这个决定在技术上是可行的。然而,通过坚持让插件作者手动实现getPluginButtonDescriptor(),该接口迫使人们明确做出决定。
7.2.3 Plugin Allocation and Deallocation
Our original problem was that the main program did not know the class name of plugin commands and therefore could not allocate them via a call to new. We solved this problem by creating an abstract Plugin interface responsible for exporting command prototypes, command names, and sufficient information for the GUI to create buttons. Of course, to implement this interface, plugins must derive from the Plugin class, thereby creating a specialization, the name of which the main application cannot know in advance. Seemingly, we have made no progress and have returned to our original problem.
我们最初的问题是,主程序不知道插件命令的类名,因此不能通过调用new来分配它们。我们通过创建一个抽象的Plugin接口来解决这个问题,该接口负责输出命令原型、命令名称以及GUI创建按钮所需的足够信息。当然,为了实现这个接口,插件必须从Plugin类派生出来,从而创建一个特殊化,主程序不能事先知道它的名字。似乎,我们没有取得任何进展,又回到了我们最初的问题。
Our new problem, similar as it may be to the original problem, is actually much easier to solve. The problem is solved by creating a single extern “C” allocation/ deallocation function pair in each plugin with prespecified names that allocate/ deallocate the Plugin specialization class via the base class pointer. To satisfy these requirements, we add the following two functions to the plugin interface:
我们的新问题,虽然可能与原问题相似,但实际上更容易解决。这个问题是通过在每个插件中创建一个单一的extern “C” 分配/去分配函数对来解决的,该函数具有预先指定的名称,通过基类指针来分配/去分配插件的专用类。为了满足这些要求,我们在插件接口中添加了以下两个函数。
extern "C" void* AllocPlugin();
extern "C" void DeallocPlugin(void*);
Obviously, the AllocPlugin() function allocates the Plugin specialization and returns it to the main application, while the DeallocPlugin() function deallocates the plugin once the main application is finished using it. Curiously, the AllocPlugin() and DeallocPlugin() functions use void pointers instead of Plugin pointers. This interface is necessary to preserve C linkage since an extern “C” interface must conform to C types. An unfortunate consequence of maintaining C linkage is the necessity of casting. The main application must cast the void to a Plugin before using it, and the shared library must cast the void back to a Plugin before calling delete. Note, however, that we do not need the concrete Plugin’s class name. Thus, the AllocPlugin()/ DeallocPlugin() function pair solves our problem.
很明显,AllocPlugin()函数分配了Plugin的特殊化,并将其返回给主程序,而DeallocPlugin()函数在主程序使用完插件后,将其取消分配。奇怪的是,AllocPlugin()和DeallocPlugin()函数使用void指针而不是Plugin指针。这个接口对于保持C语言的联系是必要的,因为一个外部的 “C “接口必须符合C类型。保持C语言联系的一个不幸的后果是必须进行铸造。主程序在使用它之前必须将void转换为Plugin,而共享库在调用delete之前必须将void转换为Plugin。然而,请注意,我们不需要具体的Plugin的类名。因此,AllocPlugin()/ DeallocPlugin()函数对解决了我们的问题。
7.2.4 The Plugin Command Interface 插件命令界面
Technically, a special plugin command interface is not necessary. However, providing such an interface facilitates writing plugin commands that obey the C++ Plugin Rules. Specifically, by creating a PluginCommand interface, we assure plugin developers of two key features. First, we provide an interface that guarantees that plugin commands do not inherit from a command class that has any state (to avoid alignment problems). This property is obvious by construction. Second, we adapt the checkPreconditionsImpl() function to create an exception-free interface across the plugin boundary. With this guidance in mind, here is the PluginCommand interface:
从技术上讲,一个特殊的插件命令接口是没有必要的。然而,提供这样一个接口可以促进编写遵守C++插件规则的插件命令。具体来说,通过创建一个PluginCommand接口,我们向插件开发者保证了两个关键特性。首先,我们提供了一个接口,保证插件命令不继承于有任何状态的命令类(以避免对齐问题)。这个属性在构造上是显而易见的。其次,我们调整了checkPreconditionsImpl()函数,以创建一个跨越插件边界的无异常接口。考虑到这一指导,这里是PluginCommand接口。
class PluginCommand : public Command {
public:
virtual ~PluginCommand();
private:
virtual const char* checkPluginPreconditions() const noexcept = 0;
virtual PluginCommand* clonePluginImpl() const noexcept = 0;
void checkPreconditionsImpl() const override final;
PluginCommand* cloneImpl() const override final;
};
While only mentioned briefly in Chapter 4, all of the pure virtual functions in the Command class are marked noexcept except for checkPreconditionsImpl() and cloneImpl() (see the sidebar on keyword noexcept). Therefore, to ensure that plugin commands do not originate exceptions, we simply implement the checkPreconditionsImpl() and cloneImpl() functions at the PluginCommand level of the hierarchy and create new, exception-free, pure virtual functions for its derived classes to implement. checkPreconditionsImpl() and cloneImpl() are both marked final in the PluginCommand class to prevent specializations from inadvertently overriding either of these functions. The implementation for checkPreconditionsImpl() can trivially be written as follows:
虽然在第四章中只是简单地提到,但除了checkPreconditionsImpl()和cloneImpl()之外,Command类中的所有纯虚函数都被标记为noexcept(见侧边栏中的关键字noexcept)。因此,为了确保插件命令不产生异常,我们只需在PluginCommand层实现checkPreconditionsImpl()和cloneImpl()函数,并为其派生类创建新的、无异常的纯虚函数来实现。checkPreconditionsImpl()和cloneImpl()在PluginCommand类中都被标记为final,以防止特殊化无意中覆盖这些函数。checkPreconditionsImpl()的实现可以简单地写成如下。
void PluginCommand::checkPreconditionsImpl() const
{
const char* p = checkPluginPreconditions();
if (p)
throw Exception(p);
return;
}
Note that the key idea behind the implementation above is that the PluginCommand class’s implementation resides in the main application’s compilation unit, while any specializations of this class reside in the plugin’s compilation unit. Therefore, via virtual dispatch, a call to checkPreconditionsImpl() executes in the main application’s compilation unit, and this function in turn calls the exception-free checkPluginPreconditions() function that resides in the plugin’s compilation unit. If an error occurs, the checkPreconditionsImpl() function receives the error via a nullptr return value and subsequently originates an exception from the main application’s compilation unit rather than from the plugin’s compilation unit.
请注意,上述实现背后的关键思想是,PluginCommand类的实现驻留在主程序的编译单元中,而该类的任何特殊化驻留在插件的编译单元中。因此,通过虚拟调度,对checkPreconditionsImpl()的调用在主程序的编译单元中执行,而这个函数反过来调用驻留在插件编译单元中的无异常的checkPluginPreconditions()函数。如果发生错误,checkPreconditionsImpl()函数通过一个nullptr返回值接收错误,并随后从主程序的编译单元而不是从插件的编译单元发起一个异常。
A similar trivial implementation for cloneImpl() can be found in Command.cpp. Plugin commands that inherit from PluginCommand instead of Command, UnaryCommand, or BinaryCommand are much more likely to avoid violating any of the C++ Plugin Rules and are therefore much less likely to generate difficult-to-diagnose, plugin-specific runtime errors.
在Command.cpp中可以找到一个类似的cloneImpl()的琐碎实现。继承自PluginCommand而不是Command、UnaryCommand或BinaryCommand的插件命令更有可能避免违反任何C++插件规则,因此更不可能产生难以诊断的特定插件运行时错误。
MODERN C++ DESIGN NOTE: NOEXCEPT
现代C++设计说明:noexcept
the C++98 standard admits using exception specifications. For example, the following specification indicates that the function foo() does not throw any exceptions (the throw specification is empty):
C++98标准承认使用异常规范。例如,下面的规范表示函数foo()不抛出任何异常(throw规范为空)。
void foo() throw();
unfortunately, many problems existed with C++98 exception specifications. While they were a noble attempt at specifying the exceptions a function could throw, they often did not behave as expected. For example, the compiler never guaranteed exception specifications at compile time but instead enforced this constraint through runtime checks. even worse, declaring a no throw exception specification could impact code performance. For these reasons and more, many coding standards were written declaring that one should simply avoid exception specifications (see, for example, standard 75 in [27]).
不幸的是,C++98的异常规范存在着许多问题。虽然它们在指定一个函数可以抛出的异常方面是一个崇高的尝试,但它们的行为往往不符合预期。例如,编译器从来没有在编译时保证异常规范,而是通过运行时检查来强制执行这一约束。由于这些原因和其他原因,许多编码标准被写出来,声明人们应该简单地避免异常规范(例如,见[27]中的标准75)。
While specifying which specifications a function can throw has proven not to be terribly useful, specifying that a function cannot throw any exceptions can be an important interface consideration. Fortunately, the C++11 standard remedied the exception specification mess by introducing the noexcept keyword. For an in-depth discussion of the uses of the noexcept specifier, see item 14 in [19]. For our discussion, we’ll concentrate on the keyword’s usefulness in design.
虽然指定一个函数可以抛出哪些规范已被证明不是非常有用,但指定一个函数不能抛出任何异常可以是一个重要的接口考虑。幸运的是,C++11标准通过引入noexcept关键字补救了异常规范的混乱。关于noexcept规范的深入讨论,请参见[19]的第14项。在我们的讨论中,我们将集中讨论这个关键字在设计中的作用。
performance optimization aside, the choice to use noexcept in a function’s specification is largely a matter of preference. For most functions, no exception specification is the norm. even if a function’s code does not itself emit exceptions, it is difficult to ensure statically that nested function calls within a function do not emit any exceptions. therefore, noexcept is enforced at runtime rather than guaranteed at compile time. thus, my personal recommendation is to reserve the usage of the noexcept specifier for particular instances where making a strong statement about the intent of a function is necessary. pdCalc’s Command hierarchy illustrates several situations where not throwing an exception is important for correct operation. this requirement is codified in the interface to inform developers that throwing exceptions will lead to runtime faults.
撇开性能优化不谈,在一个函数的规范中选择使用noexcept在很大程度上是一个偏好问题。即使一个函数的代码本身不产生异常,也很难静态地保证函数中的嵌套函数调用不产生任何异常。因此,noexcept在运行时强制执行,而不是在编译时保证。因此,我个人的建议是保留noexcept指定符的使用,以备在特殊情况下对函数的意图做出强有力的声明。pdCalc的命令层次说明了在一些情况下,不抛出异常对于正确的操作是很重要的。
7.2.5 API Versioning API版本管理
Invariably, over the lifetime of a long-lived application, the specification for plugins may change. This implies that a plugin written at one point in time may no longer function with an updated API version. For an application shipped as a single unit, the components composing the whole (i.e., the multiple shared libraries) are synchronized by the development schedule. For a complete application, versioning is used to express to the external world that the overall application has changed. However, because plugins are designed to be standalone from the main application’s development, synchronizing plugin releases with application releases may be impossible. Furthermore, the plugin API may or may not change with each application release. Therefore, to ensure compatibility, we must version the plugin API separately from the main application. While you may not anticipate changing the plugin API in the future, if you don’t add the ability to query plugins for their supported API version upfront as part of the API itself, you’ll have to introduce a breaking change to add this feature later. Depending on your requirements, such a breaking change may not be feasible, and you’ll never be able to add API versioning. Therefore, even if it’s not used initially, adding a function to query a plugin’s supported API version in the plugin interface should be considered an implicit requirement. As is hopefully apparent, the API version is distinct from the application’s version.
不可避免的是,在一个长期存在的应用程序的生命周期中,插件的规范可能会改变。这意味着,在某个时间点上编写的插件可能在更新的API版本中不再发挥作用。对于一个作为单一单元发货的应用程序来说,组成整体的组件(即多个共享库)是由开发计划同步的。对于一个完整的应用程序,版本控制被用来向外部世界表达整个应用程序已经改变。然而,由于插件被设计成独立于主应用程序的开发,将插件发布与应用程序发布同步可能是不可能的。此外,插件的API可能会也可能不会随着每个应用程序的发布而改变。因此,为了确保兼容性,我们必须将插件的API与主应用程序分开发布。虽然你可能不会预期在未来改变插件的API,但如果你不在前期添加查询插件支持的API版本的能力,作为API本身的一部分,你将不得不引入一个突破性的变化来添加这个功能。根据你的要求,这样的破坏性改变可能不可行,你将永远无法添加API版本。因此,即使最初没有使用,在插件界面中添加一个函数来查询插件支持的API版本,也应该被视为一个隐含的需求。希望这一点是显而易见的,API版本与应用程序的版本是不同的。
The actual API version numbering scheme can be as simple or as complicated as is deemed appropriate. On the simple side, it can be a single integer. On the more complicated side, it can be a structure containing several integers for major version, minor version, etc. For pdCalc, I chose a simple structure utilizing only a major version and a minor version number. The interface code is given by the following:
实际的API版本编号方案可以很简单,也可以很复杂,只要认为合适。在简单的方面,它可以是一个单一的整数。在更复杂的方面,它可以是一个包含几个整数的结构,代表主要版本和次要版本,等等。对于pdCalc,我选择了一个简单的结构,只利用一个主要版本和一个次要版本号。接口代码由以下内容给出。
class Plugin {
public:
struct ApiVersion {
int major;
int minor;
};
virtual ApiVersion apiVersion() const = 0;
};
Because pdCalc is at its first release, the main application requires no algorithm more sophisticated than checking that a plugin is using API version 1.0. If this constraint is violated, the offending plugin is not loaded.
因为pdCalc是第一次发布,主程序不需要比检查一个插件是否使用1.0版本的API更复杂的算法。如果这个约束被违反,违规的插件就不会被加载。
7.2.6 Making the Stack Available 让堆栈可用
Part of the plugin interface consists of making plugins and their commands discoverable to pdCalc. The other part of pdCalc’s plugin interface consists of making necessary parts of pdCalc’s functionality available to plugins. Specifically, the implementation of new commands requires access to pdCalc’s stack.
插件接口的一部分是使插件和它们的命令能够被pdCalc发现。pdCalc的插件接口的另一部分是使插件可以使用pdCalc的必要功能部分。具体来说,新命令的实现需要访问pdCalc的栈。
As we saw when we developed the core commands, commands require only very basic access to the stack. Specifically, they need the ability to push elements onto the stack, to pop elements off of the stack, and potentially to inspect elements from the stack (to implement preconditions). Our strategy for making this functionality available to the core commands was to implement the Stack class as a singleton with a public interface that included push, pop, and inspection member functions. However, this design fails to extend to plugin commands because it violates two of the C++ Plugin Rules. Namely, our current interface does not conform to C linkage (the stack provides a C++ class interface) and the current inspection function returns stack elements via an STL vector.
正如我们在开发核心命令时看到的,命令只需要对堆栈进行非常基本的访问。具体来说,它们需要将元素推入堆栈、从堆栈中弹出元素的能力,以及从堆栈中检查元素的潜在能力(以实现前提条件)。我们使这些功能对核心命令可用的策略是将堆栈类实现为一个具有公共接口的单例,包括推送、弹出和检查成员函数。然而,这种设计无法扩展到插件命令,因为它违反了C++插件规则中的两条。也就是说,我们目前的接口不符合C语言的链接(堆栈提供了一个C++类的接口),而且目前的检查函数通过STL向量返回堆栈元素。
The solution to this problem is quite trivial. We simply add a new interface to the stack (preferably in a specially designated header file) consisting of a collection of global (outside the pdCalc namespace) extern “C” functions that translate between C linkage and C++ class linkage (the adapter pattern again). Recall that since the Stack class was implemented as a singleton, neither the plugins nor the global helper functions need to own a Stack reference or pointer. The helper functions directly access the Stack through its Instance() function. I chose to implement the following five functions in a separate StackPluginInterface.h header file:
这个问题的解决方案是非常微不足道的。我们只需为堆栈添加一个新的接口(最好是在一个特别指定的头文件中),由全局(在pdCalc命名空间之外)外部 “C “函数的集合组成,在C链接和C++类链接之间进行转换(又是适配器模式)。回顾一下,由于Stack类被实现为一个单例,无论是插件还是全局帮助函数都不需要拥有一个Stack引用或指针。辅助函数通过Instance()函数直接访问Stack。我选择在一个单独的StackPluginInterface.h头文件中实现以下五个函数。
extern "C" void StackPush(double d, bool suppressChangeEvent);
extern "C" double StackPop(bool suppressChangeEvent);
extern "C" size_t StackSize();
extern "C" double StackFirstElement();
extern "C" double StackSecondElement();
For simplicity, since my example plugin did not need deeper access to the stack than the top two elements, I created only two inspection functions, StackFirstElement() and StackSecondElement(), for getting the top two elements of the stack. If desired, a function returning the elements of the stack to any depth could have been implemented. To maintain extern “C” linkage, the implementer of such a function would need to remember to use a raw array of doubles rather than an STL vector.
为了简单起见,由于我的例子插件不需要对堆栈进行比前两个元素更深的访问,我只创建了两个检查函数,StackFirstElement()和StackSecondElement(),用于获取堆栈的前两个元素。如果需要的话,还可以实现一个返回任何深度的堆栈元素的函数。为了保持extern “C”链接,这样一个函数的实现者需要记住使用一个原始的双数数组,而不是STL向量。
The complete, straightforward implementations for the above five functions appear in the StackPluginInterface.cpp file. As an example, the implementation of the StackSize() function is given by the following:
上述五个函数的完整、直接的实现出现在StackPluginInterface.cpp文件中。作为一个例子,StackSize()函数的实现是由以下内容给出的:
size_t StackSize()
{
return pdCalc::Stack::Instance().size();
}
7.3 Problem 2: Loading Plugins 问题2:加载插件
As previously stated, plugins are platform specific, and, inherently, the loading of plugins requires platform-specific code. In this section, we will consider two topics. First, we’ll address the platform-specific code necessary for loading libraries and their respective symbols. Here, we’ll look at two platform interfaces: POSIX (Linux, UNIX, Mac OS X) and win32 (MS Windows). Second, we’ll explore a design strategy to mitigate the source code clutter often arising from the use of platform-specific code.
在本节中,我们将考虑两个主题。首先,我们将讨论加载库和它们各自的符号所需的平台特定代码。在这里,我们将看一下两个平台接口。POSIX(Linux、UNIX、Mac OS X)和win32(MS Windows)。第二,我们将探讨一种设计策略,以减轻因使用平台特定代码而经常出现的源代码混乱。
7.3.1 Platform-Specific Plugin Loading 特定平台的插件加载
In order to work with plugins, we only need three platform-specific functions: a function to open a shared library, a function to close a shared library, and a function to extract symbols from an opened shared library. Table 7-1 lists these functions and their associated header file by platform. Let’s look at how these functions are used.
为了使用插件,我们只需要三个特定平台的函数:一个打开共享库的函数,一个关闭共享库的函数,以及一个从打开的共享库中提取符号的函数。表7-1按平台列出了这些函数和它们相关的头文件。让我们来看看这些函数是如何使用的。
Table 7-1. Plugin functions for different platforms
7.3.2 Loading, Using, and Closing a Shared Library 加载、使用和关闭一个共享库
The first step in using a plugin is asking the runtime system to open the library and make its exportable symbols available to the current working program. The open command on each platform requires the name of the shared library to be opened (POSIX also requires a flag specifying the desired symbol binding, either lazy or immediate), and it returns an opaque handle to the library, which is used to refer to the library in subsequent function calls. On a POSIX system, the handle type is a void, while on a win32 system, the handle type is an HINSTANCE (which, after some unraveling, is a typedef for a void). As an example, the following code opens a plugin library, libPlugin.so, on a POSIX system:
使用插件的第一步是要求运行时系统打开库,使其可导出的符号对当前工作程序可用。每个平台上的打开命令都需要被打开的共享库的名称(POSIX还需要一个标志,指定所需的符号绑定,无论是懒惰的还是即时的),并且它返回一个不透明的库的句柄,用于在随后的函数调用中引用该库。在POSIX系统中,句柄类型是一个void,而在win32系统中,句柄类型是一个HINSTANCE(经过一些解读,它是一个`void`的类型定义)。作为一个例子,下面的代码在POSIX系统上打开了一个插件库,libPlugin.so。
void* handle = dlopen("libPlugin.so", RTLD_LAZY);
where the RTLD_LAZY option simply tells the runtime system to perform lazy binding, which resolves symbols as the code that references them is executed. The alternative option is RTLD_NOW, which resolves all undefined symbols in the library before dlopen() returns. The null pointer is returned if the open fails. A simple error handling scheme skips loading any functionality from a null plugin, warning the user that opening the plugin failed.
其中RTLD_LAZY选项简单地告诉运行时系统执行懒惰绑定,即在执行引用它们的代码时解析符号。另一个选项是RTLD_NOW,它在dlopen()返回之前解决库中所有未定义的符号。如果打开失败,则返回空指针。一个简单的错误处理方案跳过了从一个空插件中加载任何功能,警告用户打开插件失败。
Aside from the different function names, the main platform-specific difference for opening a plugin is the canonical naming convention employed by the different platforms. For example, on Linux, shared libraries begin with lib and have a .so file extension. On Windows, shared libraries (usually called dynamically linked libraries, or simply DLLs) have no particular prefix and a .dll file extension. On Mac OS X, shared libraries conventionally are prefaced with lib and have the .dylib extension. Essentially, this naming convention matters only in two places. First, the build system should create plugins with an appropriate name for the respective platform. Second, the call to open a plugin should specify the name using the correct format. Since plugin names are specified at runtime, we need to ensure that plugin names are correctly specified by the user supplying the plugin.
除了不同的函数名称外,打开一个插件的主要特定平台差异是不同平台采用的标准命名惯例。例如,在Linux上,共享库以lib开头,文件扩展名为.so。在Windows上,共享库(通常称为动态链接库,或简单的DLLs)没有特定的前缀,文件扩展名为.dll。在Mac OS X上,共享库通常以lib为前缀,扩展名为.dylib。从本质上讲,这种命名惯例只在两个地方重要。首先,构建系统应该为各自的平台创建具有适当名称的插件。第二,打开一个插件的调用应该使用正确的格式指定名称。由于插件的名称是在运行时指定的,我们需要确保插件的名称是由提供插件的用户正确指定的。
Once a plugin has been opened, we’ll need to export symbols from the shared library in order to call the functions contained within the plugin. This export is accomplished by calling either dlopen() or LoadLibrary() (depending on the platform), either of which uses a plugin function’s string name to bind the plugin function to a function pointer. The bound plugin function is then called in the main application indirectly via this obtained function pointer.
一旦一个插件被打开,我们就需要从共享库中导出符号,以便调用插件中包含的函数。这种输出是通过调用dlopen()或LoadLibrary()来完成的(取决于平台),其中任何一个都使用插件函数的字符串名称来将插件函数绑定到一个函数指针上。然后在主程序中通过这个获得的函数指针间接地调用绑定的插件函数。
In order to bind to a symbol in the shared library, we need to have a plugin handle (the return value from opening a plugin), to know the name of the function in the plugin we want to call, and to know the signature of the function we want to call. For pdCalc, the first plugin function we need to call is AllocPlugin() to allocate the embedded Plugin class (see Section 7.2.3 above). Because this function is declared as part of the plugin interface, we know both its name and its signature. As an example, on Windows, for an already loaded plugin pointed to by HINSTANCE handle, we bind the plugin’s AllocPlugin() function to a function pointer with the following code:
为了绑定到共享库中的一个符号,我们需要有一个插件句柄(打开一个插件的返回值),知道我们要调用的插件中的函数名称,并知道我们要调用的函数的签名。对于pdCalc来说,我们需要调用的第一个插件函数是AllocPlugin(),用来分配嵌入式插件类(见上面7.2.3节)。因为这个函数被声明为插件接口的一部分,我们知道它的名字和签名。举个例子,在Windows上,对于一个已经加载的由HINSTANCE handle指向的插件,我们用下面的代码将该插件的AllocPlugin()函数绑定到一个函数指针上:
// function pointer of AllocPlugin 's type:
extern "C" {
typedef void* (*PluginAllocator)(void);
}
// bind the symbol from the plugin
auto alloc = GetProcAddress(handle, "AllocPlugin");
// cast the symbol from void* (return of GetProcAddress)
// to the function pointer type of AllocPlugin
PluginAllocator allocator { reinterpret_cast<PluginAllocator>(alloc) };
Subsequently, the plugin’s Plugin specialization is allocated by the following:
随后,该插件的Plugin专业化由以下方面分配:
// only dereference if the function was bound properly
if (allocator) {
// dereference the allocator, call the function,
// cast the void* return to a Plugin*
auto p = static_cast<Plugin*>((*allocator)());
}
The concrete Plugin is now available for use (e.g., loading plugin commands, querying the supported plugin API) through the abstract Plugin interface.
具体的Plugin现在可以通过抽象的Plugin接口来使用(例如,加载插件命令,查询支持的插件API)。
An analogous sequence of code is required to bind and execute the plugin’s DeallocPlugin() function upon plugin deallocation. The interested reader is referred to the platform-specific code in the GitHub repository for the details. Remember that before deallocating a plugin, since commands allocated by the plugin are resident in memory in the main application (but must be reclaimed in the plugin), a plugin must not be closed until all of its commands are freed. Examples of plugin commands resident in the main application’s memory space are command prototypes in the CommandRepository and commands on the undo/redo stack in the CommandManager.
类似的代码序列需要在插件去分配时绑定并执行插件的DeallocPlugin()函数。有兴趣的读者可以参考GitHub仓库中的特定平台代码以了解细节。请记住,在去分配一个插件之前,由于插件分配的命令驻留在主程序的内存中(但必须在插件中回收),在所有的命令被释放之前,不得关闭一个插件。驻留在主程序内存空间中的插件命令的例子是CommandRepository中的命令原型和CommandManager中撤销/重做堆栈中的命令。
Since a plugin is an acquired resource, we should release it when we are finished using it. This action is performed on a POSIX platform by calling dlclose() and on a win32 platform by calling FreeLibrary(). For example, the following code for a POSIX system closes a shared library (the handle) that was opened with dlopen():
由于插件是一种获得的资源,我们应该在使用完后释放它。这个动作在POSIX平台上通过调用dlclose()进行,在win32平台上通过调用FreeLibrary()进行。例如,下面的代码在POSIX系统中关闭了一个用dlopen()打开的共享库(句柄)。
// only try to close a non-null library
if(handle)
dlclose(handle);
Now that we have discussed the platform-specific mechanics of opening, using, and closing plugins, let’s turn our attention to a design strategy that mitigates the inherent complications of working with multi-platform source code.
现在我们已经讨论了打开、使用和关闭插件的特定平台机制,让我们把注意力转移到减轻多平台源代码工作的固有复杂性的设计策略上。
7.3.3 A Design for Multi-Platform Code 多平台代码的设计
Portability across platforms is a laudable goal for any software project. However, achieving this goal while maintaining a readable codebase requires significant forethought. In this section, we’ll examine some design techniques for achieving platform portability while maintaining readability.
跨平台的可移植性是任何软件项目的一个值得称道的目标。然而,在实现这一目标的同时保持代码库的可读性,需要进行大量的预先考虑。在这一节中,我们将研究一些设计技巧,以便在保持可读性的同时实现平台可移植性。
7.3.3.1 The Obvious Solution: Libraries 明显的解决方案:库
The obvious (and preferred) solution to the portability problem is to use a library that abstracts platform dependencies for you. Using a high quality library for any development scenario always saves you the effort of having to design, implement, test, and maintain functionality required for your project. Using a library for cross-platform development has the additional benefit of hiding platform-specific code behind a platform-independent API. Such an API, of course, allows you to maintain a single codebase that works seamlessly across multiple platforms without littering the source code with preprocessor directives. Although I did not explicitly discuss these merits in Chapter 6, Qt’s toolkit abstraction provides a platform-independent API for building a GUI, an otherwise platform-dependent task. In pdCalc, we used Qt to build a GUI that compiles and executes on Windows and Linux (and, presumably OS X, although I have not verified that fact) without changing a single line of the source code between platforms.
解决可移植性问题的明显(也是首选)方法是使用一个为你抽象出平台依赖性的库。在任何开发场景中使用一个高质量的库,总是可以节省你设计、实现、测试和维护项目所需功能的精力。使用库进行跨平台开发有一个额外的好处,那就是将特定平台的代码隐藏在独立于平台的API后面。当然,这样的API允许你维护一个可以在多个平台上无缝工作的单一代码库,而不需要用预处理程序指令来扰乱源代码。尽管我在第6章中没有明确讨论这些优点,但Qt的工具包抽象为构建GUI提供了一个独立于平台的API,否则这将是一项依赖平台的任务。在pdCalc中,我们用Qt建立了一个GUI,它可以在Windows和Linux上编译和执行(可能还有OS X,虽然我没有验证过),而不需要在不同的平台上改变任何一行源代码。
Alas, the obvious solution is not always available. Many reasons exist for not incorporating libraries in a project. First, many libraries are not free, and the cost of a library may be prohibitive, especially if the license has usage fees in addition to development fees. Second, a library’s license may be incompatible with a project’s license. For example, maybe you are building a closed source code, but the only available library has an incompatible open source license (or vice versa). Third, libraries are frequently shipped without source code. Lacking source code makes extending a library’s functionality impossible. Fourth, you might require support for a library, but the vendor might not supply any. Fifth, a library may ship with an upgrade cycle incompatible with your own. Sixth, a library might be incompatible with your toolchain. Finally, a library may not exist at all for the functionality you are seeking. Therefore, while using a library typically is the first choice to achieve portability, enough counterexamples to using a library exist to merit discussing how to achieve portability without one.
唉,明显的解决方案并不总是可用的。不在项目中加入库的原因有很多。首先,许多库不是免费的,而且库的成本可能很高,特别是如果许可证除了开发费以外还有使用费。第二,一个库的许可证可能与项目的许可证不兼容。例如,也许你正在构建一个封闭的源代码,但唯一可用的库有一个不兼容的开放源代码许可证(反之亦然)。第三,库经常是在没有源代码的情况下发货的。缺少源代码使得扩展一个库的功能成为不可能。第四,你可能需要一个库的支持,但供应商可能不提供任何支持。第五,一个库的升级周期可能与你自己的不兼容。第六,一个库可能与你的工具链不兼容。最后,一个库可能根本就不存在你所寻求的功能。因此,尽管使用一个库通常是实现可移植性的第一选择,但存在足够多的使用库的反例,值得讨论如何在没有库的情况下实现可移植性。
7.3.3.2 Raw Preprocessor Directives 原始预处理程序指令
Using raw preprocessor directives is undoubtedly the first method tried when attempting to achieve cross-platform code. Nearly everyone who has written portable code probably started this way. Simply, everywhere platform dependent code appears, the platform-specific pieces are surrounded by preprocessor #ifdef directives. Let’s take, for example, the runtime loading of a shared library in both Linux and Windows:
原始的预处理程序指令几乎所有写过可移植代码的人都可能是从这种方式开始。简单地说,凡是出现了依赖平台的代码,都会用预处理器的#ifdef指令来包围特定平台的部分。让我们举个例子,在Linux和Windows中运行时加载一个共享库:
#ifdef POSIX
void* handle = dlopen("libPlugin.so", RTLD_LAZY);
#elif WIN32
HINSTANCE handle = LoadLibrary("Plugin.dll");
#endif
Don't forget the preprocessor directives surrounding the header files too:
#ifdef POSIX
#include <dlfcn.h>
#elif WIN32
#include <windows.h>
#endif
For a small number of platforms or for a very few instances, using raw preprocessor directives can be tolerable. However, this technique scales poorly. As soon as either the number of platforms or the number of code locations requiring platform-dependent code increases, using raw preprocessor directives quickly becomes a mess. The code becomes difficult to read, and finding all the platform-dependent locations when adding a new platform becomes a nightmare. In even a medium sized project, sprinkling the code with #ifdefs quickly becomes untenable.
对于少数平台或极少数实例,使用原始预处理器指令是可以容忍的。然而,这种技术的扩展性很差。只要平台的数量或需要依赖平台的代码位置的数量增加,使用原始预处理器指令很快就会变得一团糟。代码变得难以阅读,当添加一个新的平台时,寻找所有与平台相关的位置成为一场噩梦。甚至在一个中等规模的项目中,在代码中撒上#ifdefs很快就会变得难以维持。
7.3.3.3 (Slightly) More Clever Preprocessor Directives (略微)更聪明的预处理程序指令
Where platform APIs are different in name but identical in function call arguments (more common than you might expect since similar functionality, unsurprisingly enough, requires similar customizations), we can be a little more clever in our usage of the preprocessor. Instead of placing the preprocessor directives at the site of every platform-dependent function call and type declaration, we can instead create platformdependent macro names and define them in a centralized location. This idea is better explained with an example. Let’s look at closing a shared library on Linux and Windows:
当平台API名称不同但函数调用参数相同时(比你想象的更常见,因为类似的功能,不出所料,需要类似的定制),我们可以在使用预处理器时更聪明一些。与其把预处理器指令放在每个平台依赖的函数调用和类型声明的地方,我们不如创建平台依赖的宏名称,并在一个集中的地方定义它们。用一个例子可以更好地解释这个想法。让我们看看在Linux和Windows上关闭一个共享库:
// some common header defining all platform dependent analogous symbols
#ifdef POSIX
#define HANDLE void*
#define CLOSE_LIBRARY dlclose
#elif WIN32
#define CLOSE_LIBRARY FreeLibrary
#define HANDLE HINSTANCE
#endif
// in the code, for some shared library HANDLE handle
CLOSE_LIBRARY(handle);
This technique is significantly cleaner than the naive approach of sprinkling #ifdefs at every function call invocation. However, it is severely limited by only working for function calls with identical arguments. Obviously, we would still need an #ifdef at the call site for opening a shared library because the POSIX call requires two arguments, while the Windows call requires only one. Certainly, with the abstraction capabilities of C++, we can do better.
这种技术比在每个函数调用中撒上#ifdefs的天真做法要干净得多。然而,由于它只适用于参数相同的函数调用,因此受到了严重的限制。很明显,我们仍然需要在打开共享库的调用处设置#ifdef,因为POSIX调用需要两个参数,而Windows调用只需要一个参数。当然,利用C++的抽象能力,我们可以做得更好。
7.3.3.4 A Build System Solution
An interesting idea that seems appealing at first is to separate platform-specific code into platform-specific source files and then use the build system to choose the correct file based on the platform. Let’s consider an example. Place all of the Unix-specific code in a file called UnixImpl.cpp, and place all of the Windows-specific code in a file called WindowsImpl.cpp. On each respective platform, code your build scripts to only compile the appropriate platform-specific file. Using this technique, no platform preprocessor directives are required since any given source file only contains source for one platform.
一个有趣的想法一开始似乎很吸引人,那就是把特定平台的代码分成特定平台的源文件,然后使用构建系统根据平台选择正确的文件。让我们考虑一个例子。把所有的Unix专用代码放在一个叫UnixImpl.cpp的文件中,把所有的Windows专用代码放在一个叫WindowsImpl.cpp的文件中。在每个平台上,对你的构建脚本进行编码,只编译相应的平台特定文件。使用这种技术,不需要平台预处理器指令,因为任何给定的源文件只包含一个平台的源代码。
The above scheme suffers from two distinct drawbacks. First, the method only works if you maintain identical interfaces (e.g., function names, class names, argument lists) to your own source code across all platform-specific files on all platforms. This feat is easier said than done, especially if you have independent teams working and testing on each platform. Compounding the problem, because the compiler only sees the code for a single platform at any given time, there is no language mechanism (e.g., type system) to enforce these cross-platform interface constraints. Second, the mechanics of achieving cross-platform compatibility are completely opaque to any developer examining the source code on a single platform. On any one platform, only one of the many platformdependent source files effectively exists, and this source file supplies no hint of the others’ existence. Of course, this latter problem exacerbates the former because the lack of cross-platform source transparency coupled with the lack of language support for the technique makes it nearly impossible to maintain interface consistency. For these reasons, a pure build system solution is intractable.
上述方案有两个明显的缺点。首先,该方法只有在你在所有平台上的所有特定平台文件中保持相同的接口(例如,函数名、类名、参数列表),才会对你自己的源代码起作用。这一壮举说起来容易做起来难,尤其是当你有独立的团队在每个平台上工作和测试时。使问题更加复杂的是,由于编译器在任何时候都只能看到单一平台的代码,因此没有语言机制(例如类型系统)来执行这些跨平台的接口约束。第二,实现跨平台兼容性的机制对于任何在单一平台上检查源代码的开发者来说是完全不透明的。在任何一个平台上,许多与平台相关的源文件中只有一个有效地存在,而且这个源文件没有提示其他的存在。当然,后一个问题加剧了前一个问题,因为缺乏跨平台的源代码透明度,再加上缺乏对该技术的语言支持,几乎不可能保持界面的一致性。由于这些原因,一个纯粹的构建系统解决方案是难以解决的。
With the downsides of this technique noted, we must be careful not to throw out the baby with the bathwater, for the kernel of our final solution lies in a language supported mechanism at the juxtaposition of both the preprocessor and the build system solutions. This design technique is examined in the following section.
注意到这种技术的缺点后,我们必须注意不要把婴儿和洗澡水一起扔掉,因为我们最终解决方案的核心在于预处理器和构建系统解决方案并列的语言支持机制。下一节将对这种设计技术进行研究。
7.3.3.5 A Platform Factory Function 一个平台工厂功能
Scattering preprocessor macros throughout a code everywhere platform-specific functionality is required is analogous to using integer flags and switch statements to execute type-specific code. Not coincidentally, both problems have the same solution, which is to build an abstract class hierarchy and execute specific functionality via polymorphism.
将预处理器宏散布在需要特定平台功能的代码中,类似于使用整数标志和开关语句来执行特定类型的代码。并非巧合的是,这两个问题都有相同的解决方案,那就是建立一个抽象的类层次结构,并通过多态性执行特定的功能。
We’ll build our solution of designing a general cross-platform architecture in two steps. First, we’ll design a platform hierarchy for handling dynamic loading. Second, we’ll extend this specific solution into a framework for abstracting platform dependence into a platform-independent interface. In both steps, we will employ a hybrid solution that utilizes the build system in a type-safe manner through a minimum use of platformspecific preprocessor directives. Along the way, we’ll encounter two additional, important design patterns: the factory method and the abstract factory. Let’s start by examining the platform-independent dynamic loading of plugins.
我们将分两步建立我们的解决方案,即设计一个通用的跨平台架构。首先,我们将设计一个平台层次结构来处理动态加载。其次,我们将把这个具体的解决方案扩展为一个框架,将平台依赖性抽象为一个独立于平台的接口。在这两个步骤中,我们将采用一种混合的解决方案,通过最低限度地使用特定于平台的预处理器指令,以类型安全的方式利用构建系统。在这一过程中,我们会遇到另外两种重要的设计模式:工厂方法和抽象工厂。让我们先来看看与平台无关的插件动态加载。
To address our specific problem, we start by first defining a platform-independent abstract interface for a DynamicLoader base class. Our DynamicLoader only needs to do two things: allocate and deallocate plugins. The base class is therefore trivially defined as follows:
为了解决我们的具体问题,我们首先为DynamicLoader基类定义了一个独立于平台的抽象接口。我们的动态加载器只需要做两件事:分配和删除插件。因此,基类被琐碎地定义如下:
class DynamicLoader{
public:
virtual ~DynamicLoader();
virtual Plugin* allocatePlugin(const string& pluginName) = 0;
virtual void deallocatePlugin(Plugin*) = 0;
}
The design intent of the above base class is that the hierarchywill be specialized byplatform.
上述基类的设计意图是,该层次结构将按平台进行专业化。
Notice that the interface itself is platform independent. The platform-dependent allocation and deallocation is an implementation detail handled by the platform-specific derived classes of this interface through the virtual functions. Furthermore, because each platform-specific implementation is wholly contained in a derived class, by placing each derived class in a separate file, we can use the build system to selectively compile only the file relevant for each platform, obviating the need for platform preprocessor directives anywhere within the hierarchy. Even better, once a DynamicLoader has been allocated, the interface abstracts away the platform-specific details of plugin loading, and the consumer of a plugin need not be concerned with plugin loading details. Loading just works. For the implementer of the derived classes of the DynamicLoader, the compiler can use type information to enforce interface consistency across platforms since each derived class must conform to the interface specified by the abstract base class, which is common to all platforms. The design is summarized pictorially in Figure 7-1. The included source code for pdCalc implements platform-specific loaders for a POSIX compliant system and for Windows.
请注意,该接口本身是独立于平台的。与平台相关的分配和去分配是一个实现细节,由这个接口的特定平台派生类通过虚拟函数处理。此外,由于每个特定平台的实现都完全包含在派生类中,通过将每个派生类放在一个单独的文件中,我们可以使用构建系统有选择地只编译与每个平台相关的文件,而不需要在层次结构的任何地方使用平台预处理器指令。更妙的是,一旦分配了DynamicLoader,接口就会抽象出插件加载的特定平台细节,插件的消费者不需要关心插件加载细节。加载只是工作。对于DynamicLoader的派生类的实现者来说,编译器可以使用类型信息来执行跨平台的接口一致性,因为每个派生类都必须符合抽象基类所指定的接口,这是所有平台所共有的。图7-1对该设计进行了形象的总结。pdCalc的源代码为符合POSIX的系统和Windows实现了特定平台的加载器。
Figure 7-1. The dynamic loader hierarchy for platform-independent plugin allocation and deallocation
图7-1. 用于独立于平台的插件分配和取消分配的动态加载器层次结构
The above design hides platform-specific details behind an abstract interface, alleviating the need for a plugin consumer to understand how a plugin is loaded. That is, of course, assuming that the plugin consumer instantiates the correct platform-specific derived class, something that cannot be handled automatically by the DynamicLoader hierarchy. Here, we introduce a new design pattern, the factory function, to solve the problem of instantiating the correct derived class. Simply, the factory function is a pattern that separates type creation from the logical point of instantiation.
上述设计将特定平台的细节隐藏在一个抽象的接口后面,减轻了插件消费者了解插件如何被加载的需要。当然,这是假设插件消费者实例化了正确的特定平台的派生类,这也是动态加载器层次结构无法自动处理的。在这里,我们引入了一种新的设计模式—工厂函数,来解决实例化正确的派生类的问题。简单地说,工厂函数是一种模式,它将类型创建与实例化的逻辑点分开。
Before progressing, I should point out the semantic difference between a factory function and the factory method pattern, as defined by the Gang of Four [6]. Simply, the factory method pattern implements a factory via a separate class hierarchy. A factory, more generally, is any mechanism of separating the selection of the specific derived class in a hierarchy from the point of logical instantiation. A factory function is a factory composed of a single function rather than a separate creational hierarchy.
在继续之前,我应该指出工厂函数与四人帮[6]所定义的工厂方法模式之间的语义差异。简单地说,工厂方法模式是通过一个独立的类层次来实现工厂。更笼统地说,工厂是将层次结构中特定派生类的选择与逻辑实例化点分离的任何机制。工厂函数是由单个函数组成的工厂,而不是单独的创建层次。
Typically, a factory is implemented by calling a function that takes a flag (an integer, an enumeration, a string, etc.) to delimit the specialization of a hierarchy and returns a base class pointer. Let’s examine an artificial example. Suppose we have a hierarchy of Shapes with the derived classes Circle, Triangle, and Rectangle. Furthermore, suppose we have defined the following enumerated class:
通常,工厂是通过调用一个函数来实现的,该函数接收一个标志(一个整数、一个枚举、一个字符串等)来划分层次结构的专业化,并返回一个基类指针。让我们来看看一个人为的例子。假设我们有一个 “形状 “的层次结构,其中有派生类 “圆”、”三角 “和 “矩形”。此外,假设我们定义了以下的枚举类:
enum class ShapeType {Circle, Triangle, Rectangle};
The following factory function could be used to create shapes:
下面的工厂函数可以用来创建形状:
unique_ptr<Shape> shapeFactory(ShapeType t)
{
switch (t) {
case ShapeType::Circle:
return make_unique<Circle>();
case ShapeType::Triangle:
return make_unique<Triangle>();
case ShapeType::Rectangle:
return make_unique<Rectangle>();
}
}
A Circle could be created by the following function call:
可以通过以下函数调用创建一个圆圈:
auto s = shapeFactory(ShapeType::Circle);
Why is the above construction anymore useful than typing
为什么上述结构比打字更有用呢
auto s = make_unique<Circle >();
Truthfully, it’s not. Instead, however, consider a factory function that accepts a string argument instead of an enumerated type (replacing the switch statement with a series of if statements). We can now construct a Circle with the following statement:
实话实说,不是这样的。然而,相反,考虑一个接受字符串参数而不是枚举类型的工厂函数(用一系列if语句替换switch语句)。我们现在可以用下面的语句构造一个Circle。
auto s = shapeFactory("circle");
The above is a much more useful construct than direct instantiation using a class name because discovery of the value of the string argument to shapeFactory() can be deferred to runtime. A typical usage of the simple factory method as described above is to enable the condition defining which specialization is instantiated to appear in a configuration file or an input file.
以上是一个比使用类名直接实例化更有用的构造,因为发现 shapeFactory() 的字符串参数的值可以推迟到运行时。上述简单工厂方法的一个典型用法是使定义哪种特殊化被实例化的条件出现在配置文件或输入文件中。
In our case, the factory is even simpler. Since our hierarchy is specialized by platform, rather than passing in a flag to choose the appropriate derived class, we simply make the selection by using preprocessor directives, as shown in Listing 7-1.
在我们的例子中,工厂甚至更简单。由于我们的层次结构是按平台划分的,因此我们不需要通过一个标志来选择适当的派生类,而是通过使用预处理程序指令来进行选择,如清单7-1所示。
Listing 7-1. A Dynamic Loader Factory Function
清单7-1. 一个动态加载器工厂函数
unique_ptr<DynamicLoader> dynamicLoaderFactory()
{
#ifdef POSIX
return make_unique<PosixDynamicLoader>();
#elif WIN32
return make_unique<WindowsDynamicLoader>();
#else
return nullptr;
}
By compiling the dynamicLoaderFactory() function into its own source file, we can achieve platform-independent plugin creation by isolating one set of preprocessor directives in one source file. The factory function is then called to return the correct type of DynamicLoader at the site where plugin allocation or deallocation is needed. By having the factory return a unique_ptr, we need not worry about memory leaks. The following code snippet illustrates the platform-independent usage of the DynamicLoader:
通过将dynamicLoaderFactory()函数编译到它自己的源文件中,我们可以通过在一个源文件中隔离一组预处理器指令来实现独立于平台的插件创建。然后调用工厂函数,在需要分配或删除插件的地方返回正确类型的DynamicLoader。通过让工厂返回一个唯一的ptr,我们不需要担心内存泄漏的问题。下面的代码片段说明了DynamicLoader独立于平台的用法:
// Question: What plaform?
auto loader = dynamicLoaderFactory();
// Answer: Who cares?
auto plugin = (loader ? loader->allocatePlugin(pluginName) : nullptr);
For the purposes of pdCalc, we could stop with the DynamicLoader hierarchy and our simple factory function. We only have the need to abstract one platform-dependent feature (the allocation and deallocation of plugins), and the code above is sufficient for this purpose. However, we’ve come this far, and it’s worth taking one extra step to see a generalized implementation of platform independence applicable to situations calling for a number of different platform-dependent features, even if it is not specifically needed for our case study.
对于pdCalc来说,我们可以停止使用DynamicLoader的层次结构和我们简单的工厂函数。我们只需要抽象出一个依赖于平台的特性(插件的分配和删除),上面的代码就足以达到这个目的。然而,我们已经走到了这一步,值得再多走一步,看看适用于需要许多不同平台依赖性功能的情况的平台独立性的通用实现,即使在我们的案例研究中不特别需要。
7.3.3.6 An Abstract Factory for Generalized Platform Independent Code 一个通用平台独立代码的抽象工厂
As software developers, we are constantly faced with design challenges caused by platform dependence. The following is an incomplete list of common platform-specific programming tasks for a C++ developer: plugin loading, interprocess communication, navigation of the file system (standardized in C++17), graphics, threading (standardized in C++11), persistent settings, binary serialization, sizeof() built-in data types, timers (standardized in C++11), and network communication. Most, if not all, of the functionality on this list can be obtained through platform-independent APIs in libraries such as boost or Qt. For me personally, the platform-specific feature that has caused the most aggravation has been the humble directory separator (/ on a Posix system and \ on a Windows system).
作为软件开发者,我们不断面临着由平台依赖性引起的设计挑战。以下是一个C++开发者常见的平台特定编程任务的不完整列表:插件加载、进程间通信、文件系统导航(在C++17中标准化)、图形、线程(在C++11中标准化)、持久化设置、二进制序列化、sizeof()内置数据类型、计时器(在C++11中标准化)和网络通信。这个列表中的大部分(如果不是全部)功能都可以通过boost或Qt等库中与平台无关的API获得。对我个人来说,引起最多困扰的平台特定功能是不起眼的目录分隔符(Posix系统上的/,Windows系统上的\)。
Suppose our calculator required the ability to read, write, and save persistent custom settings (see Chapter 8 for some reasons why this might be necessary for a calculator). Typically, Linux systems save settings in text files (for example, on Ubuntu, user settings are saved in files in the .config directory in home), while on Windows systems, persistent settings are saved in the system registry. In practice, the best solution would be to use an existing library that has already implemented this abstraction (e.g., Qt’s QSettings class). For instructional purposes, we’ll assume that no external libraries are available, and we’ll examine a design that adds persistent settings (or any number of platform-dependent functionality) alongside our existing dynamic loader. Our focus will be on the abstraction rather than the specifics of the settings implementation on each platform.
假设我们的计算器需要读取、写入和保存持久的自定义设置的能力(参见第8章,了解一些对计算器来说可能是必要的原因)。通常,Linux系统将设置保存在文本文件中(例如,在Ubuntu中,用户设置保存在home中的.config目录下的文件中),而在Windows系统中,持久化设置保存在系统注册表中。在实践中,最好的解决方案是使用一个已经实现了这种抽象的现有库(例如,Qt的QSettings类)。为了教学的目的,我们将假设没有外部库可用,我们将研究一个设计,在我们现有的动态加载器旁边增加持久化设置(或任何数量的平台依赖功能)。我们的重点将放在抽象上,而不是每个平台上的设置实现的具体细节。
The easy solution is to piggyback on our dynamic loader and simply add the necessary settings interface directly into the DynamicLoader class. Of course, we would need to rename the class to something more generic, such as OsSpecificFunctionality, with derived classes such as LinuxFuntionality and WindowsFunctionality. This method is simple, fast, and quickly intractable; it is antithetical to cohesion. For any sizable code, this technique eventually leads to uncontrollable bloat and thus a complete lack of maintainability of the interface. Despite time pressures on projects, I recommend always avoiding this quick solution because it merely increases your technical debt and causes longer delays in the future than would be experienced in the present with a proper solution.
简单的解决方案是在我们的动态加载器上搭便车,直接在动态加载器类中添加必要的设置接口。当然,我们需要把这个类重命名为更通用的东西,比如OsSpecificFunctionality,以及LinuxFuntionality和WindowsFunctionality等派生类。这种方法简单、快速,而且很快就难以解决;它与内聚性背道而驰。对于任何有规模的代码,这种技术最终会导致不可控制的臃肿,从而使界面完全缺乏可维护性。尽管项目有时间上的压力,我建议总是避免这种快速的解决方案,因为它只是增加了你的技术债务,并在未来造成比现在用适当的解决方案所能经历的更长时间的延迟。
Instead of bloating our existing DynamicLoader class, we instead take inspiration from its design and create a separate, analogous settings hierarchy as depicted in Figure 7-2. Again, we have the problem of instantiating a platform-specific derived class on each unique platform. However, instead of adding an additional settingsLoaderFactory() function to mirror the existing dynamicLoaderFactory() function, we instead seek a generalized solution that enables indefinite functional extension while preserving the single code point for platform selection. As expected, we are not the first programmers to encounter this particular problem and a solution already exists: the abstract factory pattern.
与其让我们现有的DynamicLoader类变得臃肿,不如从它的设计中获得灵感,创建一个单独的、类似的设置层次,如图7-2所示。同样,我们有一个问题,就是在每个独特的平台上实例化一个特定平台的派生类。然而,我们没有增加一个额外的settingsLoaderFactory()函数来反映现有的dynamicLoaderFactory()函数,而是寻求一种通用的解决方案,在保留平台选择的单一代码点的同时,实现无限的功能扩展。不出所料,我们不是第一个遇到这个特殊问题的程序员,而且已经有了一个解决方案:抽象工厂模式。
Figure 7-2. The settings hierarchy for platform-independent persistent settings
图7-2. 独立于平台的持久性设置的设置层次结构
According to Gamma et al [6], an abstract factory “provides an interface for creating families of related or dependent objects without specifying their concrete classes.” Essentially, the pattern can be constructed in two steps:
- Create independent hierarchies (families) for each of the related objects (e.g., a dynamic loader hierarchy and a settings hierarchy, related by their platform dependence).
- Create a hierarchy, specializing on the dependent relationship (e.g., the platform), that provides factory functions for each of the families.
根据Gamma等人[6]的说法,抽象工厂 “为创建相关或依赖对象的家族提供了一个接口,而不需要指定它们的具体类别”。从本质上讲,该模式可以通过两个步骤构建:
- 为每个相关的对象创建独立的层次结构(族)(例如,动态加载器层次结构和设置层次结构,通过它们的平台依赖性进行关联)。
- 创建一个专门针对依赖关系(如平台)的层次结构,为每个族提供工厂功能。
I find the above abstraction very difficult to comprehend without a concrete example; therefore, let’s consider the problem we are trying to solve in pdCalc. As you walk through this example, refer to the (overly complex) class diagram in Figure 7-3. Recall that the overall goal of this abstraction is to create a single source location capable of providing a platform-independent mechanism for creating any number of platform specific specializations.
我发现如果没有一个具体的例子,上述的抽象概念是很难理解的;因此,让我们考虑一下我们在pdCalc中试图解决的问题。当你走过这个例子时,请参考图7-3中的(过于复杂的)类图。回顾一下,这个抽象的总体目标是创建一个单一的源位置,能够提供一个独立于平台的机制来创建任何数量的平台特定的专业。
Figure 7-3. The abstract factory pattern applied to pdCalc
As we’ve already seen, the platform-dependent functionality can be abstracted into parallel, independent hierarchies. These hierarchies enable platform-dependent implementations to be accessed through platform-independent base class interfaces via polymorphism. For pdCalc, this pattern translates into providing platformagnostic Settings and DynamicLoader hierarchies to abstract persistent settings and dynamic loading, respectively. For example, we can allocate and deallocate a plugin polymorphically through the abstract DynamicLoader interface, provided the system instantiates the correct underlying derived class (PosixDynamicLoader or WindowsDynamicLoader) based on the platform. This part of the abstract factory is represented by the DynamicLoader hierarchy in Figure 7-3.
正如我们已经看到的,依赖平台的功能可以被抽象成平行的、独立的层次结构。这些层次结构使得依赖平台的实现可以通过多态性,通过与平台无关的基类接口被访问。对于pdCalc来说,这种模式转化为提供与平台无关的Settings和DynamicLoader层次结构,分别抽象出持久化设置和动态加载。例如,我们可以通过抽象的DynamicLoader接口多态地分配和删除一个插件,只要系统根据平台实例化正确的底层派生类(PosixDynamicLoader或WindowsDynamicLoader)。抽象工厂的这一部分由图7-3中的DynamicLoader层次结构表示。
The problem now reduces to instantiating the correct derived class based on the current platform. Instead of providing separate factory functions to instantiate the DynamicLoader and Settings objects (a decentralized approach requiring separate platform #ifdefs in each factory), we instead create a hierarchy that provides an abstract interface for providing the factory functions necessary to create the DynamicLoader and Settings objects. This abstract factory hierarchy (the PlatformFactory hierarchy in Figure 7-3) is then specialized on the platform so that we have platform-specific derived classes of the factory hierarchy that create platform-specific derived classes of the functional hierarchies. This scheme centralizes the platform dependence into a single factory function that instantiates the correct PlatformFactory specialization. In pdCalc’s implementation, I chose to make the PlatformFactory a singleton and thereby “hide” the PlatformFactory’s factory function in the Instance() function.
现在的问题简化为根据当前的平台实例化正确的派生类。我们没有提供单独的工厂函数来实例化DynamicLoader和Settings对象(这种分散的方法要求每个工厂都有单独的平台#ifdefs),而是创建了一个层次结构,为创建DynamicLoader和Settings对象所需的工厂函数提供了一个抽象接口。这个抽象的工厂层次结构(图7-3中的PlatformFactory层次结构)在平台上被专门化,这样我们就有了工厂层次结构的特定平台派生类,这些派生类创建了功能层次结构的特定平台派生类。这个方案将平台的依赖性集中到一个工厂函数中,该函数实例化了正确的PlatformFactory专业化。在pdCalc的实现中,我选择让PlatformFactory成为单例,从而将PlatformFactory的工厂函数 “隐藏 “在Instance()函数中。
The abstract factory pattern might still not make a lot of sense, so let’s look at some sample code, viewing the abstraction in a top-down fashion. Ultimately, the abstract factory pattern enables us to write the following platform-independent, high-level code in pdCalc:
抽象工厂模式可能仍然没有什么意义,所以让我们看看一些示例代码,以一种自上而下的方式来看待抽象。最终,抽象工厂模式使我们能够在pdCalc中写出以下独立于平台的高级代码:
// PlatformFactory Instance returns either a PosixFactory or a
// WindowsFactory instance (based on the platform), which in turn
// creates the correct derived DynamicLoader
auto loader = PlatformFactory::Intance().createDynamicLoader();
// The correctly instantiated loader provides platform specific
// dynamic loading functionality polymorphically through a platform
// independent interface
auto plugin = loader -> allocatePlugin(pName);
// ...
loader->deallocatePlugin(plugin);
// Same principle for settings ...
auto settings = PlatformFactory::Instance().createSettings();
settings->readSettingsFromDisk();
// ...
settings->commitSettingsToDisk();
Drilling down, the first function we’ll examine is the PlatformFactory’s Instance() function, which returns either a PosixFactory or a WindowsFactory, depending on the platform.
往下看,我们要研究的第一个函数是PlatformFactory的Instance()函数,它根据平台的不同,返回PosixFactory或WindowsFactory。
PlatformFactory& PlatformFactory::Instance()
{
#ifdef POSIX
static PosixFactory instance;
#elif WIN32
static WindowsFactory instance;
#endif
return instance;
}
The above function is doing something subtle but clever, and it’s a trick worth knowing. From the client’s perspective, PlatformFactory looks like an ordinary singleton class. One calls the Instance() function, and a PlatformFactory reference is returned. Clients then use the PlatformFactory’s public interface as they would any other singleton class. However, because the Instance() member function returns a reference, we are free to use the instance polymorphically. Since PosixFactory and WindowsFactory both derive from PlatformFactory, the instance variable instantiated is the specialization that matches the platform as defined by the #ifdef in the implementation. We have cleverly disguised an implementation detail, the mechanics of the abstract factory pattern, from the user of the class. Unless the client noticed that the factory functions in the PlatformFactory are pure virtual, he would probably not realize he was consuming an object-oriented hierarchy. Of course, the goal is not to hide anything from the user in a nefarious plot to obscure the implementation. Rather, this information hiding is utilized to reduce the complexity burden on the client of the PlatformFactory.
上面的函数在做一些微妙但聪明的事情,这是一个值得了解的技巧。从客户的角度来看,PlatformFactory看起来就像一个普通的单例类。人们调用Instance()函数,然后返回一个PlatformFactory引用。客户端然后像使用其他单例类一样使用PlatformFactory的公共接口。然而,由于Instance()成员函数返回一个引用,我们可以自由地使用该实例的多态性。由于PosixFactory和WindowsFactory都派生自PlatformFactory,所以实例变量的实例化是与实现中的#ifdef所定义的平台相匹配的专业化。我们巧妙地掩盖了一个实现细节,即抽象工厂模式的机制,不让类的用户看到。除非客户注意到PlatformFactory中的工厂函数是纯虚拟的,否则他可能不会意识到他在消费一个面向对象的层次结构。当然,我们的目标并不是向用户隐藏任何东西,以达到掩盖实现的邪恶阴谋。相反,这种信息隐藏是用来减少PlatformFactory客户端的复杂性负担的。
We next examine the trivial implementations of the createDynamicLoader() functions in the PosixFactory and WindowsFactory classes (note the covariant return type of the functions):
我们接下来检查PosixFactory和WindowsFactory类中createDynamicLoader()函数的琐碎实现(注意这些函数的共变返回类型)。
unique_ptr<DynamicLoader> PosixFactory::createDynamicLoader()
{
return make_unique<PosixDynamicLoader>();
}
unique_ptr<DynamicLoader> WindowsFactory::createDynamicLoader()
{
return make_unique<WindowsDynamicLoader>();
}
Above, we’ve simply replaced the dynamic loader factory function (see Listing 7-1) by a class hierarchy, replacing the platform #ifdefs with polymorphism. With only one piece of functionality dependent on the platform, the replacement of a factory function with an abstract factory is certainly overkill. However, for our example, we have the independent DynamicLoader and Settings families both dependent on the same platform criterion (in principle, we could have any number of such hierarchies), and the abstract factory pattern allows us to centralize the platform dependency in one location (here, in the PlatformFactory’s Instance() function) instead of scattering it through multiple independent factory functions. From a maintenance standpoint, the value proposition is similar to preferring polymorphism to switch statements.
上面,我们只是用一个类的层次结构替换了动态加载器的工厂函数(见清单7-1),用多态性替换了平台的#ifdefs。由于只有一项功能依赖于平台,用抽象工厂替换工厂函数无疑是矫枉过正。然而,在我们的例子中,我们有独立的DynamicLoader和Settings家族,它们都依赖于同一个平台标准(原则上,我们可以有任何数量的这种层次结构),而抽象工厂模式允许我们将平台依赖性集中在一个地方(这里是PlatformFactory的Instance()函数中),而不是分散在多个独立的工厂函数中。从维护的角度来看,其价值主张类似于喜欢多态性而不是开关语句。
The final pieces of the puzzle are the implementations of both the DynamicLoader and Settings hierarchies. Fortunately, these implementations are identical to the ideas outlined in Section 7.3.3, and I need not repeat their implementations here. Using the abstract factory pattern indeed adds no inherent complication to the implementation of platform-dependent functions. The pattern only adds mechanics around the instantiation of these classes via a single factory hierarchy instead of a sequence of factory functions.
拼图的最后一块是DynamicLoader和Settings层次结构的实现。幸运的是,这些实现与第7.3.3节中概述的想法相同,我无需在此重复其实现。使用抽象工厂模式确实没有为实现依赖平台的函数增加任何固有的复杂性。该模式只是围绕着这些类的实例化,通过单一的工厂层次结构而不是一连串的工厂函数增加了一些机械性。
In the source code in pdCalc’s repository, no Settings hierarchy (or its associated readSettingsFromDisk() and commitSettingsToDisk() functions in PlatformFactory) implementation exists because pdCalc, as written, has no need for a persistent settings abstraction. The Settings hierarchy was merely manufactured as a plausible example to demonstrate concretely the mechanics and relevance of the abstract factory pattern. That said, I did opt to include a full abstract platform factory implementation in pdCalc’s code for the DynamicLoader alone just to illustrate a practical implementation of the abstract factory pattern even though a simpler single factory function would have sufficed and been preferred for production code.
在pdCalc的源代码中,不存在任何Settings层次结构(或其相关的PlatformFactory中的readSettingsFromDisk()和commitSettingsToDisk()函数)的实现,因为pdCalc在编写时不需要一个持久的设置抽象概念。设置的层次结构只是作为一个合理的例子来制造,以具体展示抽象工厂模式的机制和相关性。尽管如此,我还是选择在pdCalc的代码中加入了一个完整的抽象平台工厂的实现,仅仅是为了说明抽象工厂模式的实际实现,尽管一个更简单的单一工厂函数就足够了,也是生产代码的首选。
7.4 Problem 3: Retrofitting pdCalc 问题3:改造pdCalc
We now turn to the final plugin problem, which is retrofitting the already developed classes and interfaces to accommodate adding calculator functionality dynamically. This problem is not about plugin management. Rather, the problem we are addressing here is extending pdCalc’s module interfaces to accept plugin features. Essentially, where Section 7.2 defined how to discover commands and buttons in a plugin, this section describes how to incorporate these newly discovered commands into pdCalc.
我们现在转向最后一个插件问题,即改造已经开发的类和接口,以适应动态添加计算器功能。这个问题不是关于插件的管理。相反,我们在这里解决的问题是扩展 pdCalc 的模块接口以接受插件功能。从本质上讲,第7.2节定义了如何发现插件中的命令和按钮,本节描述了如何将这些新发现的命令纳入pdCalc。
Let’s begin by creating an interface to enable the injection of newly discovered plugin commands. Recall from Chapter 4 how core commands are loaded into the CommandRepository when the application starts. First, the main application calls the RegisterCoreCommands() function. Second, within this function, the registerCommand() function of the CommandRepository class is called for each core command, registering the command’s name and a command prototype with the CommandRepository. In Section 7.2, we developed an interface for exporting command names and command prototypes from plugins. Obviously, to register these plugin commands, we simply need to extend the command dispatcher module’s interface to include the registerCommand() function. This interface extension, however, raises an interesting question. What does it mean to extend a module interface in C++, a language with no formal module definition?
让我们从创建一个接口开始,以实现对新发现的插件命令的注入。回顾一下第四章,当应用程序启动时,核心命令是如何加载到CommandRepository中的。首先,主程序调用RegisterCoreCommands()函数。其次,在这个函数中,CommandRepository类的registerCommand()函数为每个核心命令被调用,在CommandRepository中注册该命令的名称和命令原型。在第7.2节中,我们开发了一个接口,用于从插件中导出命令名称和命令原型。很明显,为了注册这些插件命令,我们只需要扩展命令调度器模块的接口,使其包括registerCommand()函数。然而,这种接口扩展提出了一个有趣的问题。在C++这种没有正式模块定义的语言中,扩展一个模块接口意味着什么?
7.4.1 Module Interfaces 模块接口
Until now, we have not rigorously defined a mechanism for implementing or describing modules. We have only loosely declared certain classes to be parts of certain modules (e.g., the CommandRepository being part of the command dispatcher module). But what does that really mean? The C++ language, as it currently stands, does not implement a module concept. Therefore, the module is essentially a meta-language concept, and the language itself gives very little help in the enforcement of a module interface. We do, however, have a few basic options for defining modules and their interfaces. First, encapsulate all classes not directly part of the interface in one source file and only expose a limited module interface in a single header file. Second, build each module into a separate DLL and use the DLL symbol export mechanism to selectively export interface functions. Third, implicitly define module boundaries via declarations in documentation and comments. Let’s explore each of these options.
到目前为止,我们还没有严格地定义一个实现或描述模块的机制。我们只是松散地宣布某些类是某些模块的一部分(例如,CommandRepository是命令调度器模块的一部分)。但这到底是什么意思呢?目前的C++语言,并没有实现模块的概念。因此,模块本质上是一个元语言的概念,而语言本身在执行模块接口方面给予的帮助很少。然而,我们确实有一些定义模块及其接口的基本选择。首先,将所有不直接属于接口的类封装在一个源文件中,只在一个头文件中公开有限的模块接口。第二,将每个模块构建成一个单独的DLL,并使用DLL符号导出机制来选择性地导出接口函数。第三,通过文档和注释中的声明隐含地定义模块的边界。让我们来探讨一下这些选项中的每一个。
7.4.1.1 Source Code Hiding 源代码隐藏
Let’s begin by discussing the encapsulation model that has always been available to C programmers: hide non-public interface classes and functions in source files and do not expose their interfaces in headers. This is the only mechanism that C++ provides for truly hiding symbols (remember, access modifiers, such as private, cannot be used for classes themselves, as would be possible in Java or C#). You’ve already seen this mechanism employed in hiding class details behind the pimpl pattern.
让我们首先讨论一下C语言程序员一直可以使用的封装模式:在源文件中隐藏非公共接口的类和函数,并且不在头文件中暴露其接口。这是C++为真正隐藏符号提供的唯一机制(记住,访问修饰符,如private,不能用于类本身,这在Java或C#中是可能的)。你已经在pimpl模式背后隐藏类的细节中看到了这种机制。
At the module level, this encapsulation scheme implies that all of the source for a module would have to reside in a single source file, and only the public parts of the module’s interface would appear in a single module header file. For example, for pdCalc’s command dispatcher module, only the CommandDispatcher and the Command interface would appear in a single header, and the definitions of the CommandDispatcher class as well as the declarations and definitions of all the concrete commands, the CommandRepository, and the CommandManager would reside in a single source file. The biggest disadvantage of source hiding is that it can lead to very large single files for complicated modules. Large files create the dual dilemmas of being difficult to read due to sheer size and having long compile times for minimal changes since all of the code must be recompiled for any change in the module. The advantages of this technique are that it can be natively implemented in the C++ language and it does not require each module to reside in a separate dynamically linked library.
在模块层面,这种封装方案意味着一个模块的所有源代码都必须存在于一个源文件中,并且只有模块接口的公共部分才会出现在一个模块头文件中。例如,对于pdCalc的命令调度器模块,只有CommandDispatcher和Command接口会出现在一个头文件中,而CommandDispatcher类的定义以及所有具体命令、CommandRepository和CommandManager的声明和定义都在一个源文件中。源码隐藏的最大缺点是它会导致复杂的模块产生非常大的单个文件。大文件造成了双重困境:一是由于体积庞大而难以阅读,二是由于模块中的任何变化都必须重新编译,所以最小的变化也会有很长的编译时间。这种技术的优点是,它可以在C++语言中自然实现,而且不需要每个模块驻留在一个单独的动态链接库中。
I have personally seen this strategy employed in at least one open source package. While it does accomplish module interface hiding from a technical perspective, the result is an entire library distributed as a single header file and a single source file. The header file is over 3,000 lines long and the source file is nearly 20,000 lines long. I cannot imagine this solution being optimally designed for readability or maintainability. This open source package, to the best of my knowledge, has a single author. Readability and maintainability for a team of developers were, therefore, unlikely to have been his primary objectives.
我亲眼看到至少有一个开源包采用了这种策略。虽然从技术角度看,它确实完成了模块接口的隐藏,但其结果是整个库以一个头文件和一个源文件的形式发布。头文件有3000多行,源文件有近20000行。我无法想象这个方案是为可读性或可维护性而优化设计的。据我所知,这个开放源码包只有一个作者。因此,对于一个开发团队来说,可读性和可维护性不太可能是他的主要目标。
7.4.1.2 DLL Hiding DLL隐藏
If you are using C++, have a large code base, and want true hiding of a module’s interface, using DLL hiding is the most reasonable option. Employing this option is, of course, outside the scope of the C++ language itself. DLL hiding is based on the operating system’s library format and implemented via compiler directives. Essentially, the programmer decorates classes or functions with special compiler directives to indicate whether a function is to be imported or exported from a DLL. The compiler then creates a DLL that only publicly exports the appropriately marked symbols, and code linking to the DLL must specify which symbols it intends to import. Since the same header must be marked as an export while compiling the DLL and as an import while compiling code using the DLL, the implementation is typically accomplished by using compiler/OS-specific preprocessor directives. For example, in Windows, the following code (or a similar variant) would be employed:
如果你使用C++,有一个大的代码库,并且希望真正隐藏一个模块的接口,使用DLL隐藏是最合理的选择。当然,采用这种选择超出了C++语言本身的范围。DLL隐藏是基于操作系统的库格式并通过编译器指令实现的。基本上,程序员用特殊的编译器指令来装饰类或函数,以表明一个函数是否要从DLL中导入或导出。然后,编译器创建一个DLL,只公开导出适当标记的符号,而链接到DLL的代码必须指定它打算导入哪些符号。由于同一个头在编译DLL时必须被标记为出口,在编译使用DLL的代码时必须被标记为进口,所以通常通过使用编译器/操作系统特定的预处理器指令来实现。例如,在Windows中,将采用以下代码(或类似的变体)。
// Declspec .h
#ifdef BUILDING_DLL
#define DECLSPEC __declspec(export)
#else
#define DECLSPEC __declspec(import)
#endif
The declaration of a function foo() that we want to export from our DLL would be written as
我们想从DLL中导出的函数foo()的声明将被写为
#include "Declspec.h"
DECLSPEC void foo();
When the DLL is built, the preprocessor macro BUILDING_DLL is defined; therefore, the DECLSPEC macro expands to declspec(export). When the DLL is used, the BUILDING_DLL macro is left undefined, and the DECLSPEC macro expands to declspec(import). Any function or class not decorated with the DECLSPEC macro remains private to the DLL. GCC implements a similar mechanism using a slightly different syntax.
当 DLL 被构建时,预处理器宏 BUILDING_DLL 被定义;因此,DECLSPEC 宏扩展为 declspec(export)。当 DLL 被使用时,BUILDING_DLL 宏没有被定义,而 DECLSPEC 宏扩展为 declspec(import)。任何没有用DECLSPEC宏装饰的函数或类都保持对DLL的私有。GCC使用稍微不同的语法实现了一个类似的机制。
Most Windows programmers are very familiar with the DLL hiding mechanism for controlling module interfaces because globally hiding symbols in DLLs is the default paradigm for Visual Studio. If a symbol is not decorated, it will not be exported from the DLL. Therefore, to make a DLL that can be called from externally (is there any other kind?), Windows programmers must manually export symbols using the declspec directive. Many UNIX programmers, however, are unfamiliar with DLL hiding because the default shared library implementation publicly exports all symbols. That is, in a typical UNIX or Linux system, no need exists to decorate symbols as an export or an import in shared library code because all symbols in a shared library are made publicly available to the calling program by the linker when the shared library is loaded. Compiler command line options can reverse the default visibility from public to private, if so desired, and symbols can be marked manually for either import or export analogously to a Windows build.
大多数Windows程序员对控制模块接口的DLL隐藏机制非常熟悉,因为全局隐藏DLL中的符号是Visual Studio的默认范式。如果一个符号没有被装饰,它就不会被从DLL中导出。因此,为了制作一个可以从外部调用的DLL(还有其他种类吗?),Windows程序员必须使用declspec指令手动导出符号。然而,许多UNIX程序员不熟悉DLL隐藏,因为默认的共享库实现公开导出所有符号。也就是说,在一个典型的UNIX或Linux系统中,没有必要在共享库代码中把符号装饰成导出或导入,因为共享库中的所有符号在加载共享库时被链接器公开提供给调用程序。如果需要的话,编译器的命令行选项可以将默认的可见性从公共的改为私人的,而且符号可以被手动标记为导入或导出,类似于Windows的构建方式。
I started this section by stating that if you want true hiding of a module’s interface for a large C++ code base, using DLL hiding is the most reasonable option. It enables a very fine level of granularity for module access control provided you are content to devote a separate DLL to each module. The main disadvantages to this technique are in readability, maintainability, and portability. Using the technique does require using compiler and operating system-specific decorators that are not part of the C++ language for each exportable function. While the extra DECLSPEC macro is not too unbearable for each function or class, the definition of the macro can get unwieldy quickly when accounting for multiple operating systems or multiple compilers. Additionally, diagnosing problems caused by forgetting to define the correct preprocessor macro when building or using a DLL can confound novice programmers. Finally, correctly implementing DLL imports and exports in the presence of template code can be nontrivial.
我在本节开始时说过,如果你想真正隐藏一个大型C++代码库中的模块接口,使用DLL隐藏是最合理的选择。只要你愿意为每个模块提供一个单独的DLL,它就能为模块访问控制提供非常精细的粒度。这种技术的主要缺点是可读性、可维护性和可移植性。使用这种技术确实需要为每个可导出的函数使用编译器和操作系统特定的装饰器,这些装饰器并不是C++语言的一部分。虽然对于每个函数或类来说,额外的DECLSPEC宏并不是太难以忍受,但在考虑到多个操作系统或多个编译器时,宏的定义会很快变得不方便。此外,在构建或使用DLL时,诊断因忘记定义正确的预处理器宏而引起的问题会让新手程序员感到困惑。最后,在有模板代码的情况下,正确实现DLL的导入和导出可能不是一件容易的事。
7.4.1.3 Implicit or Documentation Hiding 隐性或文件隐藏
The technique that I have termed implicit hiding is nothing more than hiding the interface by not documenting it. What does this mean in practice? Since the C++ language does not directly support modules, implicit hiding simply draws a logical construct around a group of classes and functions, and declares those classes to compose a module. The language allows any public function of any class to be called from code external to the module. Therefore, the module’s public interface is “declared” by only documenting those functions that should be called from the outside. From a purely technical perspective, implicit hiding is no hiding at all!
我所称的隐性隐藏技术,无非是通过不记录接口来隐藏接口。这在实践中是什么意思呢?由于C++语言并不直接支持模块,隐式隐藏只是在一组类和函数周围画了一个逻辑结构,并声明这些类组成了一个模块。该语言允许从模块外部的代码中调用任何类的任何公共函数。因此,模块的公共接口是通过只记录那些应该从外部调用的函数来 “声明 “的。从纯粹的技术角度来看,隐式隐藏根本就不是隐藏!
Why would anyone choose implicit hiding over either source code hiding or DLL hiding? Quite simply, the choice is made for expedience. Using implicit hiding allows developers to organize classes and source code in a logical, readable, and maintainable style. Each class (or group of closely related classes) can be grouped into its own header and source file pair. This enables minimal inclusion of only necessary code, which leads to faster compile times. Implicit hiding also does not force the boundary definitions for inclusion into a particular shared library, which could be important if there is a design goal of minimizing the number of individual shared libraries shipped with a package.
为什么有人会选择隐式隐藏而不是源代码隐藏或DLL隐藏?很简单,这个选择是为了方便。使用隐式隐藏可以让开发者以一种逻辑的、可读的、可维护的方式组织类和源代码。每个类(或一组密切相关的类)都可以被归入它自己的头文件和源文件对。这使得只包含必要的代码成为可能,从而导致更快的编译时间。隐式隐藏也不会强制将边界定义纳入一个特定的共享库中,如果有一个设计目标是尽量减少随包运送的单个共享库的数量,这一点可能很重要。
The problem with implicit hiding is, of course, that no language mechanism exists to prevent the misuse of functions and classes not intended by the designer to be used outside of a logical module. Is this a serious problem or not? Why might we want to forcibly prevent users from calling part of the interface not deemed public? The main design reason is that we do not want users to rely upon undocumented features since the nonpublic interface is subject to change. Unsurprisingly, this reason is identical to why we prize encapsulation in class design. That is, implementations should be allowed to change independently of interfaces. So, how important is it to forcibly hide the nonpublic interface? Ultimately, it depends on how much you trust the users of your code to either not call undocumented interfaces or at least accept ownership of the unplanned maintenance forced upon them by changes to undocumented interfaces.
当然,隐性隐藏的问题在于,没有任何语言机制可以防止设计者不打算在逻辑模块之外使用的函数和类的滥用。这到底是不是一个严重的问题呢?为什么我们可能要强行阻止用户调用不被认为是公共的界面的一部分呢?主要的设计原因是,我们不希望用户依赖未记录的功能,因为非公开的接口是可以改变的。不出所料,这个原因与我们在类的设计中奖励封装的原因是一样的。也就是说,应该允许实现独立于接口而变化。那么,强行隐藏非公共接口有多重要呢?归根结底,这取决于你有多信任你的代码的使用者,他们要么不调用无文档的接口,要么至少接受因无文档接口的变化而强加给他们的计划外维护。
7.4.1.4 Module Design for pdCalc pdCalc的模块设计
I chose to use implicit hiding in the design of pdCalc. For this project, I felt that the benefits of simplicity outweighed the complications necessary to use one of the other modes of module interface hiding. Which technique you choose for your own projects will naturally reduce to your personal preferences. Given the relatively small code base of pdCalc, the choice to use implicit hiding enables grouping classes by logic rather than along module boundaries. Furthermore, implicit hiding allows lumping several of the modules (e.g., command dispatcher, stack, and plugin management) into one shared, back-end library.
我在设计pdCalc的时候选择了隐式隐藏。在这个项目中,我觉得简单的好处超过了使用其他模式的模块接口隐藏所必须的复杂程度。你为自己的项目选择哪种技术,自然要根据你的个人喜好来决定。鉴于pdCalc的代码库相对较小,选择使用隐式隐藏可以按逻辑而不是按模块边界对类进行分组。此外,隐式隐藏允许将几个模块(例如,命令调度器、堆栈和插件管理)归入一个共享的后端库。
My choice to use implicit hiding has a direct implication for solving the original problem of extending the command dispatcher module’s interface to include the registerCommand() function from the CommandRepository class. This extension can simply be accomplished by decree, or, more precisely, by a documentation change. Essentially, this function can be added to the interface by updating Table 2-2 in Chapter 2.
我选择使用隐式隐藏,对解决最初的问题有直接影响,即扩展命令调度器模块的接口,以包括CommandRepository类中的registerCommand()函数。这个扩展可以简单地通过法令来完成,或者更准确地说,通过修改文档来完成。基本上,这个函数可以通过更新第二章的表2-2添加到接口中。
Implicit hiding does not have a specific language supported feature, so you cannot point to one specific class and say, “This header defines the module’s interface.” Instead, documentation is used to draw an implicit line around selected pieces of the public classes and functions that define a module’s interface. Therefore, once the documentation has been changed, the main() function can inject plugin commands during plugin loading by calling CommandRepository’s existing registerCommand() function. No code change is needed to retrofit pdCalc for plugin command injection.
隐式隐藏没有特定的语言支持的功能,所以你不能指着一个特定的类说:”这个头定义了模块的接口”。相反,文档被用来在定义模块接口的公有类和函数的选定部分周围画一条隐式线。因此,一旦文档被修改,main()函数可以在插件加载过程中通过调用CommandRepository现有的registerCommand()函数注入插件命令。不需要改变代码来改造pdCalc以实现插件命令注入。
7.4.2 Adding Plugin Buttons to the GUI 在GUI中添加插件按钮
Recall at the outset of this section that we outlined two problems to be solved in retrofitting pdCalc for plugins. The first problem, which we just solved, was how to add plugin commands to the CommandRepository after a plugin is loaded. The solution turned out to be quite trivial since we had already written the necessary function and needed only to extend the module’s defined public interface. The second problem involves retrofitting pdCalc to be able to add buttons to the GUI that correspond to plugin commands.
记得在本节一开始,我们概述了在为插件改造pdCalc时需要解决的两个问题。第一个问题,我们刚刚解决了,就是如何在插件加载后将插件命令添加到CommandRepository中。由于我们已经写好了必要的函数,只需要扩展模块的定义的公共接口,所以解决这个问题是非常简单的。第二个问题是对pdCalc进行改造,使其能够在GUI中添加对应于插件命令的按钮。
By the design of our command dispatcher, once a command is registered, it can be executed by any user interface raising a commandEntered() event with the command’s name as the event’s argument. Hence, for the CLI, a plugin command can be executed by the user by typing in its name. That is, plugin commands become immediately accessible to the CLI as soon as they are registered. Making a plugin command accessible in the GUI, of course, is slightly more complicated because a button that can raise a commandEntered() event must be created for each discovered plugin command.
根据我们的命令调度器的设计,一旦一个命令被注册,它就可以被任何用户界面执行,引发一个commandEntered()事件,事件的参数是命令的名字。因此,对于CLI来说,一个插件命令可以由用户通过输入它的名字来执行。也就是说,插件命令一经注册,就可以立即被CLI访问。当然,让一个插件命令在GUI中被访问要稍微复杂一些,因为必须为每个发现的插件命令创建一个可以引发commandEntered()事件的按钮。
In Section 7.2, we defined an interface for labeling CommandButtons. Each plugin provides a PluginButtonDescriptor that defines the primary command label, the secondary command label, and the underlying commands associated with the labels. Therefore, in order to add a new GUI button corresponding to a plugin command, we must simply extend the interface of the GUI’s MainWindow class to include a function for adding buttons based on their labels:
在第7.2节中,我们定义了一个用于标记CommandButtons的接口。每个插件都提供了一个PluginButtonDescriptor,它定义了主要的命令标签、次要的命令标签,以及与标签相关的底层命令。因此,为了添加一个对应于插件命令的新的GUI按钮,我们必须简单地扩展GUI的MainWindow类的接口,以包括一个根据标签添加按钮的函数。
class MainWindow : public QMainWindow, public UserInterface {
public:
// Existing interface plus the following:
void addCommandButton(const string& dispPrimaryCmd,
const string& primaryCmd, const string& dispShftCmd,
const string& shftCmd);
};
Of course, this function will also need to lay out the buttons based on some suitable algorithm. My trivial algorithm simply places buttons left to right with four buttons in a row.
当然,这个函数也需要根据一些合适的算法来布置按钮。我的微不足道的算法只是将按钮从左到右排列,一排有四个按钮。
As was briefly mentioned in Chapter 6, the MainWindow class also includes a setupFinalButtons() function and a fixSize() function. The setupFinalButtons() function adds the undo, redo, and proc (see Chapter 8) buttons as the top row in the GUI. The fixSize() function forces the GUI’s geometry to stay fixed at its current dimensions. These operations can only logically be called after all plugin buttons have been added.
正如第6章中简要提到的,MainWindow类还包括setupFinalButtons()函数和fixSize()函数。setupFinalButtons()函数将撤销、重做和proc(见第8章)按钮添加到GUI的顶行。fixSize()函数强制GUI的几何形状固定在其当前尺寸。这些操作在逻辑上只有在所有的插件按钮都被添加之后才能被调用。
Unlike the registerCommand() function of the CommandRegistry, the addCommandButton() was not a preexisting public function of the MainWindow class. Therefore, we must add and implement this new function. In all likelihood, a modular implementation of the GUI would have already had a similar function somewhere in the GUI module, as this functionality was already required to create buttons for core commands. Therefore, implementation of the addCommandButton() function might be as trivial as forwarding this call from the MainWindow to the appropriate internal GUI class, where the function may have already existed.
与CommandRegistry的registerCommand()函数不同,addCommandButton()并不是MainWindow类中预先存在的一个公共函数。因此,我们必须添加和实现这个新函数。很有可能,GUI的模块化实现在GUI模块的某个地方已经有了一个类似的函数,因为这个功能已经需要为核心命令创建按钮。因此,实现addCommandButton()函数可能就像把这个调用从MainWindow转发到适当的内部GUI类一样简单,而这个函数可能已经存在了。
7.5 Incorporating Plugins 融入插件
Thus far, we have discussed guidelines for C++ plugins, the plugin interface, plugin command memory management, loading and unloading plugins, design patterns for abstracting platform-dependent code behind interfaces, and retrofitting pdCalc to enable plugin command and GUI injection. However, we have yet to discuss any mechanism for finding plugins, actually loading and unloading plugins from disk, managing the lifetime of plugins, or injecting plugin functionality into pdCalc. These operations are performed by a PluginLoader class and the main() function of the application, both of which are now described.
到目前为止,我们已经讨论了C++插件的指导方针、插件接口、插件命令的内存管理、加载和卸载插件、在接口后面抽象出依赖平台的代码的设计模式,以及改造pdCalc以实现插件命令和GUI注入。然而,我们还没有讨论任何寻找插件的机制,实际从磁盘加载和卸载插件,管理插件的生命周期,或将插件功能注入pdCalc。这些操作是由PluginLoader类和应用程序的main()函数来完成的,现在对这两个函数进行描述。
7.5.1 Loading Plugins 加载插件
Loading plugins is accomplished by a PluginLoader class. The PluginLoader is responsible for finding plugin dynamic library files, loading the plugins into memory, and serving the concrete Plugin specializations to pdCalc, on demand. The PluginLoader is also responsible for deallocating plugin resources at the appropriate times. As we’ve seen previously, a good design will implement automatic deallocation via RAII.
加载插件是由PluginLoader类完成的。PluginLoader负责寻找插件的动态库文件,将插件加载到内存中,并根据需求为pdCalc提供具体的插件特性。PluginLoader还负责在适当的时候取消分配插件资源。正如我们之前看到的,一个好的设计会通过RAII实现自动去分配。
The first step in loading plugins is determining which plugins should be loaded and when. Really, only two practical options exist to answer this question. Either plugins are loaded automatically by pdCalc when the program starts (e.g., files specified in a configuration file or all DLLs in a specific directory), or plugins are loaded on demand by direct user requests. Of course, these options are not mutually exclusive, and a PluginLoader class could be designed that incorporates both options, possibly with the ability for the user to direct which manually loaded plugins should be automatically loaded in the future. There is no right or wrong answer to how plugins are loaded. The decision must be addressed by the program’s requirements.
加载插件的第一步是确定哪些插件应该被加载以及何时加载。实际上,要回答这个问题只有两个实用的选择。一种是在程序启动时由pdCalc自动加载插件(例如,配置文件中指定的文件或特定目录中的所有DLL),另一种是由用户直接请求加载插件。当然,这些选项并不是相互排斥的,可以设计一个PluginLoader类,将这两个选项都纳入其中,可能还可以让用户指示哪些手动加载的插件应该在未来被自动加载。对于如何加载插件,没有正确或错误的答案。这个决定必须由程序的要求来解决。
For simplicity, I chose to implement a plugin loader that automatically loads plugins during pdCalc’s startup. The PluginLoader finds these plugins by reading an ASCII configuration file comprised of lines of text each individually listing the file name of a plugin. The configuration file is arbitrarily named plugins.pdp, and this file must be located in the current executable path. Plugin files listed in plugins.pdp can be specified using either a relative or absolute path. A more sophisticated plugin loader implementation would probably store the location of the plugins file in an operating system-specific configuration location (e.g., the Windows registry) and use a better file format, such as XML. A good library, like Qt, can help you parse XML and find systemspecific configuration files using a platform-independent abstraction.
为了简单起见,我选择实现一个插件加载器,在pdCalc的启动过程中自动加载插件。PluginLoader通过读取一个ASCII格式的配置文件来找到这些插件,该文件由几行文字组成,每一行都单独列出了一个插件的文件名。这个配置文件被任意命名为plugins.pdp,而且这个文件必须位于当前的可执行路径中。plugins.pdp中列出的插件文件可以用相对路径或绝对路径来指定。一个更复杂的插件加载器实现可能会将插件文件的位置存储在操作系统特定的配置位置(例如,Windows注册表),并使用更好的文件格式,如XML。一个好的库,比如Qt,可以帮助你解析XML,并使用一个与平台无关的抽象来找到系统特定的配置文件。
With the above plugin loader design constraints in mind, the PluginLoader interface is quite trivial:
考虑到上述插件加载器的设计限制,PluginLoader的接口是相当琐碎的。
class PluginLoader {
public:
void loadPlugins(UserInterface& ui, const string& pluginFileName);
const vector<const Plugin*> getPlugins();
};
The loadPlugins() function takes the name of the configuration file as input, loads each library into memory, and allocates an instance of each library’s Plugin class. The UserInterface reference is solely for error reporting. When the main() function is ready to inject the plugins’ commands, the getPlugins() function is called to return a collection of loaded Plugins. Of course, the loadPlugins() and getPlugins() functions could be combined, but I prefer a design that enables the programmer to retain finer tuned control over the timing of plugin loading versus plugin usage. My implementation of the PluginLoader makes use of a few clever techniques for using RAII to manage the automatic deallocation of the plugins. As the implementation here is orthogonal to the design, the interested reader is referred to the PluginLoader.cpp source file for details.
loadPlugins()函数将配置文件的名称作为输入,将每个库加载到内存中,并为每个库的Plugin类分配一个实例。UserInterface的引用只是为了报错。当main()函数准备好注入插件的命令时,getPlugins()函数被调用以返回已加载插件的集合。当然,loadPlugins()和getPlugins()函数可以合并,但我更喜欢这样的设计,它能使程序员对插件加载的时间和插件的使用保持更精细的控制。我对PluginLoader的实现利用了一些巧妙的技术,使用RAII来管理插件的自动去分配。由于这里的实现与设计是正交的,有兴趣的读者可以参考PluginLoader.cpp源文件了解详情。
7.5.2 Injecting Functionality 注入功能
Having decided that plugins should be loaded automatically from a configuration file, the most logical placement for plugin loading is somewhere in the main() function call tree. Essentially, this loadPlugins() function simply puts together all of the pieces we have previously discussed: loading plugin libraries, loading plugins, extracting commands and buttons from the plugin descriptors, and injecting these commands and buttons into pdCalc. Of course, a proper implementation will also perform error checking on the plugins. For example, error checking might include checking the plugin API version, ensuring the commands have not already been registered, and ensuring the GUI buttons correspond to commands in the command repository. Listing 7-2 is a skeleton of a function for loading plugins. Its inputs are a UserInterface reference for reporting errors and a PluginLoader reference.
在决定了插件应该从配置文件中自动加载后,插件加载最合理的位置是在main()函数调用树的某处。从本质上讲,这个loadPlugins()函数简单地将我们之前讨论过的所有部分放在一起:加载插件库,加载插件,从插件描述符中提取命令和按钮,并将这些命令和按钮注入到pdCalc。当然,一个正确的实现也会对插件进行错误检查。例如,错误检查可能包括检查插件的API版本,确保命令还没有被注册,以及确保GUI按钮与命令库中的命令相对应。清单7-2是一个加载插件的函数的骨架。它的输入是一个用于报告错误的UserInterface引用和一个PluginLoader引用。
Listing 7-2. A Fuction for Loading Plugins
清单 7-2. 加载插件的函数
void setupPlugins(UserInterface& ui, PluginLoader& loader)
{
loader.loadPlugins(ui, "plugins.pdp");
auto plugins = loader.getPlugins();
for (auto p : plugins) {
auto apiVersion = p->apiVersion();
// verify plugin API at correct level
// inject plugin commands into CommandRepository - recall
// the cloned command will auto release in the plugin
auto descriptor = p->getPluginDescriptor();
for (int i = 0; i < descriptor.nCommands; ++i) {
registerCommand(ui, descriptor.commandNames[i],
MakeCommandPtr(descriptor.commands[i]->clone()));
}
// if gui, setup buttons
auto mw = dynamic_cast<MainWindow*>(&ui);
if (mw) {
auto buttonDescriptor = p->getPluginButtonDescriptor();
if (buttonDescriptor) {
for (int i = 0; i < buttonDescriptor->nButtons; ++i) {
auto b = *buttonDescriptor;
// check validity of button commands
mw->addCommandButton(b.dispPrimaryCmd[i], b.primaryCmd[i],
b.dispShftCmd[i], b.shftCmd[i]);
}
}
}
}
return;
}
After a long chapter describing how to implement C++ plugins, the denouement is somewhat anticlimactic, as most of the mechanics are handled at deeper layers of the abstraction. Of course, this “boringness,” as you’ve learned in this book, is only achieved through meticulous design. Simplicity is always more difficult to achieve than the code itself indicates. Had any complications leaked through at this high-level abstraction, it would surely have implied an inferior design.
在描述如何实现C++插件的长篇大论之后,结局有些反常,因为大部分的机制都是在更深的抽象层处理的。当然,这种 “无趣”,正如你在本书中学到的那样,只有通过细致的设计才能实现。简单性总是比代码本身所显示的更难实现。如果有任何复杂的问题在这个高层的抽象中泄露出来,那肯定会意味着一个低劣的设计。
7.6 A Concrete Plugin 具体插件
After a long discussion explaining how to incorporate native C++ plugins into pdCalc, we’ve finally reached the point where we can implement a concrete plugin. Based on our requirements from Chapter 1, we need to write a plugin that adds commands for the natural logarithm, its inverse exponentiation algorithm, and the hyperbolic trigonometric functions. Of course, you should feel free to add plugins encompassing any functionality you might like. For example, two interesting plugins might be a probability plugin and a statistics plugin. The probability plugin could compute permutations, combinations, factorials, and random numbers, while the statistics plugin could compute mean, median, mode, and standard deviation. For now, however, we’ll simply consider the design and implementation of our hyperbolic, natural log plugin.
经过长时间的讨论,解释了如何将本地C++插件纳入pdCalc,我们终于达到了可以实现一个具体插件的程度。根据第一章的要求,我们需要编写一个插件,为自然对数、其反指数算法和双曲三角函数添加命令。当然,你应该自由地添加插件,包括你可能喜欢的任何功能。例如,两个有趣的插件可能是一个概率插件和一个统计插件。概率插件可以计算排列、组合、阶乘和随机数,而统计插件可以计算平均值、中位数、模式和标准差。不过,现在我们只考虑设计和实现我们的双曲、自然对数插件。
7.6.1 Plugin Interface 插件界面
The implementation of the HyperbolicLnPlugin is actually quite straightforward. We’ll begin with the interface for the class and then, uncharacteristically, examine a few implementation details. The code chosen for further examination highlights particular details relevant to native C++ plugins.
HyperbolicLnPlugin的实现实际上是非常简单的。我们将从该类的接口开始,然后一反常态地考察一些实现细节。为进一步检查而选择的代码突出了与本地C++插件有关的特殊细节。
The interface for HyperbolicLnPlugin is given by the class definition specializing the Plugin class and the required plugin allocation and deallocation functions; see Listing 7-3.
HyperbolicLnPlugin的接口是由专门的Plugin类定义和所需的插件分配和去分配函数给出的,见清单7-3。
Listing 7-3. The Interface for HyperbolicLnPlugin
清单7-3. HyperbolicLnPlugin的接口
class HyperbolicLnPlugin : public pdCalc::Plugin {
class HyperbolicLnPluginImpl;
public:
HyperbolicLnPlugin();
~HyperbolicLnPlugin();
const PluginDescriptor& getPluginDescriptor() const override;
const PluginButtonDescriptor* getPluginButtonDescriptor()
const override;
pdCalc::Plugin::ApiVersion apiVersion() const;
private:
unique_ptr<HyperbolicLnPluginImpl> pimpl_;
};
extern "C" void* AllocPlugin();
extern "C" void DeallocPlugin(void*);
As expected, the class implements the three pure virtual functions in the Plugin class and defers the bulk of its implementation to a private implementation class. The AllocPlugin() and DeallocPlugin() functions have their obvious implementations. The AllocPlugin() simply returns a new HyperbolicLnPlugin instance, while the DeallocPlugin() function casts its void argument to a Plugin and subsequently calls delete on this pointer. Note that plugins are, by definition, not part of the main program and should therefore not be part of the pdCalc namespace. Hence, the explicit namespace qualification in a few locations.
正如预期的那样,该类实现了Plugin类中的三个纯虚函数,并将其大部分实现推迟到一个私有实现类中。AllocPlugin()和DeallocPlugin()函数有其明显的实现。AllocPlugin()简单地返回一个新的HyperbolicLnPlugin实例,而DeallocPlugin()函数将其void参数转换为一个Plugin,并随后调用这个指针进行删除。注意,根据定义,插件不是主程序的一部分,因此不应该是pdCalc命名空间的一部分。因此,在一些地方有明确的命名空间限定。
The responsibility of the HyperbolicLnPluginImpl class is simply to serve plugin descriptors on demand and manage the lifetime of the objects needed by the descriptors. The PluginDescriptor provides command names and the corresponding Commands implemented by the plugin. These Commands are described in Section 7.6.3 below. The PluginButtonDescriptor for the plugin simply lists the names of the Commands as defined by the PluginDescriptor and the corresponding labels to appear on the GUI buttons. Because the commands in the HyperbolicLnPlugin all have natural inverses, we simply label each button with a forward command and attach the secondary (shifted) command to the inverse. I used the obvious labels for the commands provided by the plugin: sinh, asinh, cosh, acosh, tanh, atanh, ln, and exp. Whether you choose ln for the primary and exp as the secondary or vice versa is simply a matter of preference.
HyperbolicLnPluginImpl类的职责只是按需提供插件描述符,并管理描述符所需对象的生命周期。PluginDescriptor提供了命令名称和由插件实现的相应命令。这些命令将在下面的7.6.3节中描述。插件的PluginButtonDescriptor只是列出了PluginDescriptor所定义的命令的名称和相应的标签,这些标签将出现在GUI按钮上。因为HyperbolicLnPlugin中的命令都有自然反转,我们只需给每个按钮贴上正向命令的标签,并将辅助(移位)命令附在反转上。我为插件提供的命令使用了明显的标签:sinh, asinh, cosh, acosh, tanh, atanh, ln, 和 exp。你是选择ln作为主命令,还是选择exp作为副命令,这只是一个偏好的问题。
For reasons already discussed, plugin descriptors transfer content without using STL containers. Where we would normally prefer to use vectors and unique_ptrs in the interface to manage resources, we are forced instead to use raw arrays. Of course, the encapsulation provided by the pimpl enables the implementation of whatever memory management scheme we desire. For the HyperbolicLnPlugin, I chose a complicated scheme of automatic memory management using strings, unique_ptrs, and vectors. The advantage of using an RAII memory management scheme is that we can be assured that the plugin will not leak memory in the presence of exceptions (namely, an out-ofmemory exception thrown during construction). Realistically, I would not expect the calculator to be executed in a low memory environment, and even if it were, it’s unclear that leaking memory during plugin allocation would matter much since the user’s likely next action in this situation would be to reboot his computer. Therefore, in retrospect, a simpler memory management scheme with naked news in the constructor and deletes in the destructor would probably have been more pragmatic.
由于已经讨论过的原因,插件描述符在传输内容时没有使用STL容器。在通常情况下,我们更倾向于在接口中使用向量和 unique_ptrs 来管理资源,但我们却不得不使用原始数组。当然,由pimpl提供的封装使得我们可以实现任何我们想要的内存管理方案。对于HyperbolicLnPlugin,我选择了一个复杂的自动内存管理方案,使用字符串、unique_ptrs和向量。使用RAII内存管理方案的好处是,我们可以保证插件在出现异常时不会泄露内存(即在构建过程中抛出的内存不足的异常)。现实上,我不希望计算器在低内存环境下执行,即使是这样,也不清楚在插件分配过程中的内存泄漏会有多大影响,因为在这种情况下,用户的下一个动作可能是重新启动他的计算机。因此,回过头来看,一个更简单的内存管理方案,在构造函数中裸露news,在析构函数中删除,可能更实用。
7.6.2 Source Code Dependency Inversion 源代码依赖性反转
Surprisingly, the above class declaration for HyperbolicLnPlugin is indeed the complete interface to the plugin. I say surprisingly because, at first glance, one might be surprised that the plugin’s interface bears no relationship on the functionality the plugin provides. Of course, this situation is exactly as it should be. The calculator functionality that the plugin provides is indeed merely an implementation detail and can be contained entirely within the plugin’s implementation file.
令人惊讶的是,上述HyperbolicLnPlugin的类声明确实是该插件的完整接口。我说出乎意料是因为,乍一看,人们可能会惊讶于该插件的接口与该插件提供的功能没有关系。当然,这种情况恰恰是应该的。插件所提供的计算器功能确实只是一个实现细节,可以完全包含在插件的实现文件中。
The above subtlety, namely that pdCalc knows only the interface to a plugin and nothing about the functionality itself, should not be overlooked. As a matter of fact, this source code dependency inversion is the entire point of plugin design. What exactly is source code dependency inversion and why is it important? To answer this question, we must first embark on a short history lesson.
上述的微妙之处,即pdCalc只知道插件的接口,而对功能本身一无所知,不应该被忽视。事实上,这种源代码的依赖性倒置是插件设计的全部意义所在。究竟什么是源代码依赖反转,为什么它很重要?为了回答这个问题,我们必须首先开始上一个简短的历史课。
Traditionally (think 1970s Fortran), code was extended by simply writing new functions and subroutines. The primary design problem with this approach was that requiring the main program to call new functions bound the main program to the concrete interface of any extension. Thus, the main program became dependent on interface changes defined by the whims of extension authors. That is, every new extension defined a new interface to which the main program had to conform. This setup was extremely brittle because the main program required constant modification to keep pace with the changes to its extensions’ interfaces. Since each new extension required unique modifications to the main program’s source code, the complexity of the main program’s code for handling extensions grew linearly with the number of extensions. If that wasn’t bad enough, adding new functionality always required recompiling and relinking the main program. In concrete terms, imagine a design for pdCalc that would require modifying, recompiling, and relinking pdCalc’s source code every time a new plugin command was added.
传统上(想想20世纪70年代的Fortran),代码是通过简单地编写新函数和子程序来扩展的。这种方法的主要设计问题是,要求主程序调用新的函数,将主程序与任何扩展的具体接口联系起来。因此,主程序变得依赖于由扩展作者的奇思妙想所定义的接口变化。也就是说,每一个新的扩展都定义了一个新的接口,主程序必须与之相适应。这种设置是非常脆弱的,因为主程序需要不断地修改以跟上其扩展接口的变化。由于每个新的扩展都需要对主程序的源代码进行独特的修改,主程序处理扩展的代码的复杂性随着扩展数量的增加而线性增长。如果这还不够糟糕,增加新的功能总是需要重新编译和重新连接主程序。具体来说,想象一下pdCalc的设计,每增加一个新的插件命令都需要修改、重新编译和重新连接pdCalc的源代码。
The above problem can be solved without object-oriented programming via function pointers and callbacks, albeit in a somewhat inelegant and cumbersome fashion. However, with the rise of object-oriented programming, specifically inheritance and polymorphism, the solution to the dependency problem was solved in a type-safe manner with language support. These techniques enabled the popularization of source code dependency inversion. Specifically, source code dependency inversion states that the main program defines an interface (e.g., the plugin interface we’ve studied in this chapter) to which all extensions must conform. Under this strategy, the extensions become subservient to the main program’s interface rather than the reverse. Hence, the main program can be extended via plugins without modifying, recompiling, or relinking the main program’s source code. More importantly, however, the interface for extensibility is dictated by the application rather than its plugins. In concrete terms, pdCalc provides the Plugin interface class to define the addition of new functionality, but pdCalc is never aware of the implementation details of its extensions. A plugin that does not conform to pdCalc’s interface is simply unable to inject new Commands.
在没有面向对象编程的情况下,上述问题可以通过函数指针和回调来解决,尽管这种方式有些不优雅和麻烦。然而,随着面向对象编程的兴起,特别是继承和多态性,在语言的支持下,依赖问题的解决方案以类型安全的方式得到了解决。这些技术使得源代码的依赖性反转得以普及。具体来说,源代码依赖反转指出,主程序定义了一个接口(例如,我们在本章中研究的插件接口),所有的扩展必须符合这个接口。在这种策略下,扩展程序成为主程序接口的附属品,而不是相反。因此,主程序可以通过插件进行扩展,而无需修改、重新编译或重新链接主程序的源代码。然而,更重要的是,可扩展性的接口是由应用程序而不是其插件决定的。具体来说,pdCalc提供了Plugin接口类来定义新功能的添加,但pdCalc从来不知道其扩展的实现细节。一个不符合pdCalc接口的插件,根本无法注入新的命令。
7.6.3 Implementing HyperbolicLnPlugin’s Functionality 实现HyperbolicLnPlugin的功能
By this stage in the game, we know that the HyperbolicLnPlugin will provide its functionality by implementing a command class for each operation. After implementing a few of these classes, one would quickly notice that all of the commands in the plugin are unary commands. Unfortunately, based on the third rule of C++ plugins (assume incompatible alignment), we cannot inherit from the UnaryCommand class and instead must inherit from the PluginCommand class. Note that our alignment assumption even precludes using the UnaryCommand class via multiple inheritance, and we must reimplement the unary command functionality in our HyperbolicLnPluginCommand base class. While this does feel a bit duplicative, the rules for C++ plugins leave us with no alternatives (although we could provide source code for a UnaryPluginCommand and a UnaryBinaryCommand, but these would have to be separately compiled with each plugin).
在游戏的这个阶段,我们知道HyperbolicLnPlugin将通过为每个操作实现一个命令类来提供其功能。在实现了几个这样的类之后,人们会很快注意到,插件中所有的命令都是单数命令。不幸的是,基于C++插件的第三条规则(假设不兼容的对齐方式),我们不能继承于UnaryCommand类,而必须继承于PluginCommand类。请注意,我们的对齐方式假设甚至排除了通过多重继承来使用UnaryCommand类,我们必须在我们的HyperbolicLnPluginCommand基类中重新实现单选命令功能。虽然这确实有点重复,但C++插件的规则让我们别无选择(尽管我们可以提供UnaryPluginCommand和UnaryBinaryCommand的源代码,但这些都必须与每个插件分开编译)。
We, therefore, finally arrive at the interface class from which all commands within the HyperbolicLnPlugin derive; see Listing 7-4.
因此,我们最终到达了接口类,HyperbolicLnPlugin中的所有命令都来自于此;见清单7-4。
Listing 7-4. The HyperbolicLnPluginCommand Class
清单7-4. HyperbolicLnPluginCommand类
class HyperbolicLnPluginCommand : public pdCalc::PluginCommand {
public:
HyperbolicLnPluginCommand() { }
explicit HyperbolicLnPluginCommand(const HyperbolicLnPluginCommand& rhs);
virtual ~HyperbolicLnPluginCommand() { }
void deallocate() override;
protected:
const char* checkPluginPreconditions() const noexcept override;
private:
void executeImpl() noexcept override;
void undoImpl() noexcept override;
HyperbolicLnPluginCommand* clonePluginImpl() const noexcept override;
virtual HyperbolicLnPluginCommand* doClone() const = 0;
virtual double unaryOperation(double top) const = 0;
double top_;
};
As with the UnaryCommand class, the HyperbolicLnPluginCommand class implements the pure virtual executeImpl() and undoImpl() commands, delegating the command operation to the pure virtual unaryOperation() function. Additionally, the HyperbolicLnPluginCommand class implements the checkPluginPreconditions() function to ensure at least one number is on the stack before the command is called. The function is protected so that a subclass can directly override the precondition function if it must implement any additional preconditions yet still call the base class’s checkPluginPreconditions() to make the unary command precondition check.
与UnaryCommand类一样,HyperbolicLnPluginCommand类实现了纯虚拟的executeImpl()和undoImpl()命令,将命令操作委托给纯虚拟的unaryOperation()函数。此外,HyperbolicLnPluginCommand类实现了checkPluginPreconditions()函数,以确保在命令被调用之前,堆栈中至少有一个数字存在。该函数是受保护的,因此,如果子类必须实现任何额外的前提条件,但仍然调用基类的checkPluginPreconditions()来进行单选命令的前提条件检查,则可以直接覆盖该前提条件函数。
The deallocate() and clonePluginImpl() functions have obvious implementations but play critical roles in the plugin. The deallocate() function is simply implemented as
deallocate()和clonePluginImpl()函数有明显的实现,但在插件中起着关键作用。deallocate()函数被简单地实现为
void HyperbolicLnPluginCommand::deallocate()
{
delete this;
}
Recall that the point of the deallocate() function is to force memory deallocation of the plugin’s commands in the plugin’s compilation unit. It is called via the CommandDeleter() function when the unique_ptr holding a command is destroyed.
回想一下,deallocate()函数的意义是在插件的编译单元中强制对插件的命令进行内存去分配。当持有一个命令的unique_ptr被销毁时,它通过CommandDeleter()函数被调用。
The clonePluginImpl() function is given by
clonePluginImpl()函数是这样给出的
HyperbolicLnPluginCommand* HyperbolicLnPluginCommand::clonePluginImpl() const noexcept
{
HyperbolicLnPluginCommand* p;
try {
p = doClone();
} catch (...) {
return nullptr;
}
return p;
}
The sole purpose of this function is to adapt the cloning of a plugin command to ensure that exceptions do not cross the memory boundary between the plugin and the main application.
这个函数的唯一目的是调整插件命令的克隆,以确保异常不会跨越插件和主程序之间的内存边界。
All that remains to complete the HyperbolicLnPlugin is to subclass HyperbolicLnPluginCommand for each mathematical operation required in the plugin and implement the few remaining pure virtual functions (unaryOperation(), doClone(), and helpMessageImpl()). At this point, the implementation of these functions is no different than the implementation of the unary functions of Chapter 4. The interested reader is referred to the source code in HyperbolicLnPlugin.cpp for details.
完成HyperbolicLnPlugin的所有工作就是为插件中需要的每一个数学操作子类化HyperbolicLnPluginCommand,并实现剩下的几个纯虚拟函数(unaryOperation(), doClone(), and helpMessageImpl())。在这一点上,这些函数的实现与第4章的单选函数的实现没有什么不同。有兴趣的读者可以参考HyperbolicLnPlugin.cpp中的源代码来了解细节。
7.7 Next Steps
After a rather long discussion about C++ plugins, and with the implementation of the hyperbolic trigonometric and natural logarithm plugin, we have completed the requirements for pdCalc set forth in Chapter 1. The calculator, as originally described, is complete! Well, version 1.0 is complete, anyway. However, as experienced software developers, we know that any “finished” product is just a temporary milestone before the customer requests new features. The next chapter handles this exact situation, where we’ll modify our design to incorporate unplanned extensions.
在对C++插件进行了相当长的讨论之后,随着双曲三角和自然对数插件的实现,我们已经完成了第一章中对pdCalc提出的要求。这个计算器,就像最初描述的那样,已经完成了! 好吧,无论如何,1.0版本是完整的。然而,作为有经验的软件开发者,我们知道任何 “完成 “的产品都只是在客户要求新功能之前的一个临时里程碑。下一章将处理这种确切的情况,我们将修改我们的设计以纳入计划外的扩展。