Pregunta

I'm having trouble understanding how to provide an API to 3rd parties in order to allow extensions for desktop applications. I understand that if I'm using a compiled language (e.g. C++), I can load dynamic libraries as extensions at runtime, provided they link against my library and provide a well-defined API that the core code calls into. The part I'm struggling with is how the API piece fits into the rest of my architecture. I would typically have my application split into multiple libraries (lib, models, ui, feature-specific libraries, etc.). But I wouldn't want to have extension developers link against all of these right? If I provide another library specifically for the extension API, how would that interact with the core application components? How do well-known extendable applications (IDEs, photo/audio/video editing apps) implement this?

¿Fue útil?

Solución

I work on an API for a video editing application. We have separate SDKs (and hence APIs) for different types of extensions. There's one set of APIs for image/video processing. There's another set of APIs for importing video of different formats into the application. There's another set of APIs for exploring the document model. Each API has a Software Development Kit (SDK) that contains a library and headers that developers can build against to make an extension. (And they include documentation and sample code to show how they work.)

Architecturally, your extension API will vary based on what type of work it needs to achieve. For image processing, there are a few main parts:

  1. Figuring out what the plug-in's capabilities are (can it render using GPU textures, or does it need bitmaps on the CPU? What color space does it work in? etc.)
  2. supplying UI elements like a name for your extension, controls the user can set, etc.
  3. giving input images to the extension and getting back the output images

Internally, when the application needs to process a frame of video, it calls an extension to read the frame from disk. Once it has the frame, it does some setup with the image processing extension. It then calls the extension to render, and the extension calls back into the application to get information like the values of the various sliders it put in the UI. It does its processing, and puts the result in the output frame. The app then tells the extension to do any per-render teardown it needs to do.

If you look at popular APIs like the Photoshop API, they supply some sort of data structure or object which an extension can call to get information from the host application or give information to it. The extension itself has to implement a certain set of functions or methods that the application will call. There's generally a well-defined flow of data between the host application and an extension, but the details of that vary by the type of plug-in, and even the specific application implementing the API.

Otros consejos

A plugin API is very much like a (fully) abstract base class. It might even be expressed as an abstract base class. You require the plugin export functions of a given name and signature. Your application then calls these functions

extern void Frobnicate(Frobber frob);
extern Bazzer MakeBazzer();

Or you define a base class, and require the plugin export a function that returns an instance of that class.

class FooPlugin 
{
public:
    virtual ~FooPlugin() = default();
    void Frobnicate(Frobber frob) = 0;
    Bazzer MakeBazzer() = 0;
}

extern FooPlugin & PluginInitialise();

It only needs to have definitions of the types used in the public interface, and can be otherwise isolated from the definitions in the rest of the program.

The particular approach you use to handle integration will depend on your language system, and performance needs. There is no one-size-fits-all answer.

If you are using C++, and have a speed critical application (e.g. passing large amounts of data in and out of the plugin), you might use a .so file (dll in windows), and have a simple abstract interface for your plugin API. This is technically challenging, as there are 'marshaling' and 'linkage' issues you have to be careful about.

Another approach that works quite well is json-messaging. You can have your plugin operate as a web-service, where you send messages (and get answers). Or a slightly simpler variant of that pattern - is pipes: you have your plugin take a json-payload argument (in stdin) and return a json-formatted result through stdout). This is not super efficient (but with care can be pretty efficient). But it has the benefit of being highly decoupled - with the plugin being implemented in any language/system, and the calling code using any other language/platform/system. {obviously there is nothing special about using json in the above, just a common way to do this, and if you asked me 10 years ago, I would have said the same thing but using xml}.

One way of making the functions of the main application available to the plugin can be studied when looking at applications like MS Word, MS Excel or Autocad, when using their plugin mechanisms:

  • they provide indeed a full programmatic API to most of the program's feature. (For the given examples, it is actually the same API which is available for VBA programs).

  • the "library specifically for the extension API" you mentioned uses a technology which supports late dynamic binding (in case of Word, Excel or Autocad it is Microsoft's COM technology).

That will allow to link the plugin just against a stub library without having to link against the full main application. The stub lib then uses dynamic linkage to communicate with the main program.

Of course, in C++ an set of abstract base classes providing an abstract interface to the parts of the main application you want to expose to plugins might be sufficient (in case you expect plugin devs to use the same compiler and runtime lib version as it is used for the main application).

I like all the answers and they show how many ways you can tackle this problem. But specifically for one part of your question:

But I wouldn't want to have extension developers link against all of these right? If I provide another library specifically for the extension API, how would that interact with the core application components?

Well, you could require plugin developers to link against some libraries to make their plugins work if you want and provided that's acceptable. I've worked on some big commercial applications though and we avoided this in every case. Our plugins don't link (statically or dynamically) to any of the libraries in our software. Our software strictly dynamically links to the plugin.

The way to do this is dynamic dispatch. If you have a function pointer pointing to a function somewhere out there in outer space, then the plugin doesn't need to link to the library providing its implementation in order to call that function. And that's basically the way the people in my domain have always done it in both C and C++. We might define a function pointer signature/return type like so:

// Returns a pointer to an interface in the system given an ID.
// Real version might define calling conventions and look a little
// more ugly.
typedef void* FetchInterface(int interface_id);

And a plugin entry point in the plugins third parties write exports a function like so:

// Imported and called by our host application.
DYLIB_EXPORT void plugin_entry_point(FetchInterface* fetch);

And that might be implemented like so:

void plugin_entry_point(FetchInterface* fetch)
{
     SomeInterface* some_interface = fetch(SomeInterface::id);
     some_interface->do_something(...);
}

And our host application calls that and the plugin fetches the necessary interfaces it wants to work with through the function pointer we provide and invokes functions on those interfaces. And SomeInterface above is just a table (struct) that contains its own function pointers.

The plugin needs to include headers from our SDK to work, of course, to see how things like struct SomeInterface are defined. However, they don't need to link to anything.

Ugh, Void Pointers and C-Like Coding in C++!

You might wonder why we have this type-unsafe C-like code for our central API instead of, say, abstract interfaces through classes with pure virtual functions (which would still avoid the linkage requirements). Unfortunately the latter is not so compatible across disparate compilers and isn't well-understood by other language FFIs.

By favoring C for our API here, we've had third parties even write plugins to our software in languages like C# and Lua using their foreign function interfaces even though we didn't design it for such purpose, since FFIs tend to favor C (they don't necessarily understand C++ concepts like function overloading, virtual functions, constructors, and destructors). It's similar to how you find people using OpenGL from so many languages now, even though it was a C API not necessarily anticipated to be used in such languages.

So we favor that kind of "near-universal compatibility" aspect of a "lowest common denominator" C API. What we do instead to make it easier to work with our kind of unsafe and low-level C API in other languages is provide wrappers, like C++ wrappers which are RAII-conforming and type safe. And when we don't do it for languages we didn't anticipate others using to write plugins, like C#, the third parties tend to write a wrapper lib on top of our C API to make it easy to write plugins in C#.

Versioning and Backwards Compatibility

Probably one of the trickier requirements is that some of the products I worked on had strong backward compatibility requirements with older plugins (we had plugins written over 15 years ago that still had to work with the software, including an adapter to make 32-bit plugins still work in 64-bit versions of our software).

For that the C-style approach with struct offers a little bit of breathing room because whatever members you add to the bottom of a struct doesn't affect the memory layout of its previous fields. So we can append new function pointer fields to the bottom of an existing table of function pointers without affecting backwards compatibility with former plugins pointing to those structs.

Every now and then there's a temptation to change existing interfaces though for new plugins in ways that go beyond appending new functionality at the bottom, and for that we have to basically create a brand new version of the interface and do some hoop-jumping under the hood to provide plugins with the proper interface. In actuality our plugins pass their versions along in the analogical FetchInterface above like so:

void plugin_entry_point(FetchInterface* fetch)
{
     // Passing the PLUGIN_VERSION along here (defined in the SDK
     // headers the plugin is using) can affect what interface is
     // returned based on the plugin version.
     SomeInterface* some_interface = fetch(PLUGIN_VERSION, SomeInterface::id);
     some_interface->do_something(...);
}

Preserving backwards compatibility with such a long legacy can be a real PITA no matter what, so it pays to be very careful with how you design your APIs to reduce the probability of having to deprecate them, since a long legacy compatibility requirement makes "deprecated" more of a suggestion to use new interfaces rather than providing developers the ability to eventually remove and stop maintaining the ancient code.

Licenciado bajo: CC-BY-SA con atribución
scroll top