Skip to content

Instantly share code, notes, and snippets.

@emoon
Last active March 21, 2023 10:13
Show Gist options
  • Save emoon/e543607e103a8d707ab45cf816b35ae8 to your computer and use it in GitHub Desktop.
Save emoon/e543607e103a8d707ab45cf816b35ae8 to your computer and use it in GitHub Desktop.
API pondering

Figuring out backwards compatible APIs

Introduction

For some of the applications I'm doing I want there to be a plugin API that people can use to expand them and it's also something I'm using myself for proper dog fooding. The issue is that I really can't settle on how to do it.

Talking about APIs here it's all about native code which has to be C compatible.

If we are talking strictly about C APIs one way to do this is to have everything passed as versioned structs to the plugin. Let me give an example.

Image there is a API to load images called ImageApi and it looks like this

typedef struct ImageApi {
  void* priv_data;
  u8* load_png(void* priv_data, const char* filename);
} ImageApi;

#define IMAGE_API_VERSION 1

To access this API we have something called an Service API That works like this

typedef struct ServiceApi {
  void* priv_data;
  ImageApi* get_image_api(void* priv_data, int version);
} ServiceApi;

Now we as user in a plugin implement a create_plugin function where there service API gets passed from the host application.

void create_plugin(ServiceApi* service) {
   ImageApi* image_api = service->get_image_api(service->priv_data, IMAGE_API_VERSION);
   // Here we can now image_api to load some fancy png images!
}

The plugin is compiled and released into the wild and people can start using it. In this case we imagine that the code was released as binary only and no source was provided or lost.

Later on the host application evoles and ImageApi and you can now say max size for pngs that are loaded.

typedef struct ImageApi {
  void* priv_data;
  u8* load_png(void* priv_data, const char* filename, int max_width, int max_height);
} ImageApi;

#define IMAGE_API_VERSION 2

Now anyone that uses the new header files we get the new API, while old plugins that request v1 of the image API will continue to work as long as the host application provides the v1 API all the time.

There are several up sides to this approach:

  1. In the C case plugins doesn't need to link against any libraries as data/functions are always passed in.
  2. Old plugins will go on working.
  3. As it's a basic C API it can be wrapped to support other langs such as Rust, etc where that implementation may have to be linked to the plugin it can be very thin and just call into the C code.

The problem

So what is the problem with the above? I feel the approach works quite well for small APIs, but I'm getting a feeling that it will become harder to maintain for larger APIs. I'm currently looking at implementing a (IM)GUI API where a single struct UI { lots of funcs } may make versioning quite hard going forward.

So what would the alternatives be?

  1. One can split the full API into smaller APIs such as Button API, Slider API, etc, but it feels the devide becomes somewhat superficial and gets introduced to just workaround issues with the current API approach.
  2. This also would mean you have several apis you need to pass around, or "re-get" like:
ButtonApi* button_api = ui_get_button_api(ui_api);
SliderApi* slider_api = ui_get_slider_api(ui_api);
ui_show_button(button_api, ...);
ui_slider(slider_api, ...);
  1. To get around the above it may be possible to introduce some global variable(s) that simple holds the pointer to each API instead. Somewhere (in the user application) there will be
// Gets initialized early at some point and can be hidden in a macro
ButtonAPi* g_ui_button_api;
SliderApi* g_slider_api;
...
// Now using the code would be just like this because they would use 
// the global function ptrs internally inside some inline functions
ui_show_button(...);
ui_slider(...);
  1. The flip side can of course just to be to do it the common way (link with lib/against the dll) and when a full new version of the API has been released users are forced to re-compile the code to be compatible (I really don't like that one much)
@emoon
Copy link
Author

emoon commented Mar 19, 2023

Sure, that would be possible, but that doesn't address the larger question which is when you have a bigger API. While you could resolve everything on a per function basis I don't think that will scale that well.

Also, it should be as easy as possible from the users perspective. It better that the complexity is more hidden on the host (i.e inside the dll side)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment