Skip to content

Commit

Permalink
DOCS: Document new plugin system
Browse files Browse the repository at this point in the history
  • Loading branch information
Krzmbrzl committed Jun 6, 2021
1 parent 9d5feb5 commit 54012c5
Show file tree
Hide file tree
Showing 12 changed files with 603 additions and 0 deletions.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ Mumble supports various languages. We are always looking for qualified people to

We are using Weblate as a translation platform. [Register on Weblate](https://hosted.weblate.org/accounts/register/), and join [our translation project](https://hosted.weblate.org/projects/mumble/).

### Writing plugins

Mumble supports general-purpose plugins that can provide functionality that is not implemented in the main Mumble application. You can find more
information on how this works and on how these have to be created in the [plugin documentation](docs/dev/plugins/README.md).

## Building

For information on how to build Mumble, checkout [the dedicated documentation](docs/dev/build-instructions/README.md).
Expand Down
49 changes: 49 additions & 0 deletions docs/dev/plugins/Bundling.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Bundling a Mumble plugin

After you have built your plugin you will have a shared library for the respective target OS. While it is possible to just give users this shared
library and tell them to install them, it might get inconvenient at times. Especially if you want to support your plugin for multiple OS.

Therefore Mumble provides a way to package your plugin in a user-friendly and cross-platform way.

It works by putting the shared libraries for the different platforms into a single zip-file and then changing the archive's file extension from `.zip`
to `.mumble_plugin`.

The archive must not contain anything besides the plugin libraries and these should follow the following naming conventions:
- The shared library should contain the name of the plugin
- followed by the OS specifier
- followed by the architecture specifier

The OS specifier may be one of the following
| **OS** | **Specifier** |
| ------ | ------------- |
| Windows | `_win` |
| Linux | `_linux` |
| macOS | `_macos` |

and the architecture specifier is one of
| **Architecture** | **Specifier** |
| x86 (64-bit) | `_x86_64` |
| x86 (32-bit) | `_i386` |
| ARM (v5) | `_armv5` |
| ARM (v6) | `_armv6` |
| ARM (v7) | `_armv7` |

Therefore a plugin library may be named for instance
```
myPlugin_linux_x86_64.so
```
or
```
myPlugin_win_i386.dll
```

Platform-specific prefixes such as `lib` on Unix-systems are allowed as well. Therefore
```
libmyPlugin_linux_x86_64.so
```
is completely equivalent to the example given above.

Note that plugin names **must not** contain version numbers. At least not when they are distributed to end users. This is because Mumble assumes that
the new version of a plugin will use a shared library with the exact same name as the old one. If this assumption is violated, the update mechanism
will not work properly causing the new and the old version of the plugin to be installed in parallel.

181 changes: 181 additions & 0 deletions docs/dev/plugins/CreatePlugin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
# Creating Mumble plugins

In the following the necessary steps to create a working Mumble plugin are outlined. These instructions cover the plain C API. If you are using a
[language binding](LanguageBindings.md) for a different programming language, different steps are usually required. Please refer to the binding's
documentation for that.

In the spirit of the classical [Hello world program](https://en.wikipedia.org/wiki/%22Hello,_World!%22_program), this guide will step you through the
creation of a "Hello Mumble" plugin.

The source code of the example plugin described here can be found in [this repository](https://github.com/mumble-voip/mumble-plugin-template). This
also intended to be used as the basis for everyone that wants to start writing a plugin without having to worry about the boilerplate themselves.


## Preparations

What you need for creating a plugin is
- A working C compiler. It does not matter which one
- The Mumble plugin framework header files which are the following (the exact version number in the filename may change depending on which API version
you intend to use):
* [MumbleAPI_v_1_0_x.h](https://github.com/mumble-voip/mumble/blob/master/plugins/MumbleAPI_v_1_0_x.h)
* [MumblePlugin_v_1_0_x.h](https://github.com/mumble-voip/mumble/blob/master/plugins/MumblePlugin_v_1_0_x.h)
* [PluginComponents_v_1_0_x.h](https://github.com/mumble-voip/mumble/blob/master/plugins/PluginComponents_v_1_0_x.h)

Although not strictly required, it usually is handy to use a build system for managing your plugin project. In this guide we'll use
[cmake](https://cmake.org/). If you have never used cmake before, have a look at [this short guide](https://stackoverflow.com/a/26007567).

All in all the following file structure is assumed to be present on your device:
```
.
├── include
│   ├── MumbleAPI_v_1_0_x.h
│   ├── MumblePlugin_v_1_0_x.h
│   └── PluginComponents_v_1_0_x.h
├── CMakeLists.txt
└── plugin.c
```

The headers in `include` are the ones listed above and the other files will be populated during this guide.


## CMakeLists.txt

The `CMakeLists.txt` file is our cmake project file that tells cmake what we expect it to do.

In it, you have to put the following:
```cmake
cmake_minimum_required(VERSION 3.15)
project(MumblePlugin
VERSION "1.0.0"
DESCRIPTION "Minimal Mumble plugin"
LANGUAGES "C"
)
add_library(plugin
SHARED
plugin.c
)
target_include_directories(plugin
PUBLIC "${CMAKE_SOURCE_DIR}/include/"
)
```

If you want to understand the details it would be best if you searched for a proper cmake tutorial. The gist of it is that we tell cmake that we want
to build a shared library from the source file `plugin.c` and that everything in the `include` directory should be includable from it.


## Writing the plugin

Now that the boilerplate is out of the way, we can start writing the actual plugin. This will be done in the `plugin.c` source file.

The first thing you should do is to include `MumblePlugin_v_1_0_x.h`. Furthermore we'll need a few more C headers that we'll include as well:
```c
#include "MumblePlugin_v_1_0_x.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
```

Furthermore every plugin needs a way to store at least the Mumble-API and its own ID. In C this can be done using global variables. Therefore go ahead
and create the respective variables in the global namespace:
```c
struct MumbleAPI_v_1_0_x mumbleAPI;
mumble_plugin_id_t ownID;
```

Both data types are defined by the API via the included headers.

As stated in the docs of the [plugin-API](PluginAPI.md) there are several functions that you must implement in your plugin. The first of these is
`mumble_init`:
```c
mumble_error_t mumble_init(mumble_plugin_id_t pluginID) {
ownID = pluginID;

if (mumbleAPI.log(ownID, "Hello Mumble") != MUMBLE_STATUS_OK) {
// Logging failed -> usually you'd probably want to log things like this in your plugin's
// logging system (if there is any)
}

return MUMBLE_STATUS_OK;
}
```
As you can see the function takes the plugin's ID as a parameter, so make sure you store that in our respective variable. As you can see our Hello
Mumble plugin will use the Mumble-API to log something in Mumble's console. Note that it is safe to access the API here already due to the rules for a
[plugin's initialization processs](PluginLifecycle.md#initialization).
The final step is to return `MUMBLE_STATUS_OK` in order to let Mumble know that the plugin's initialization was successfull.
The next function to be implement is `mumble_shutdown` which is structured very similarly to `mumble_init`:
```c
void mumble_shutdown() {
if (mumbleAPI.log(ownID, "Goodbye Mumble") != MUMBLE_STATUS_OK) {
// Logging failed -> usually you'd probably want to log things like this in your plugin's
// logging system (if there is any)
}
}
```

Next up is `mumble_getName`:
```c
struct MumbleStringWrapper mumble_getName() {
static const char *name = "HelloMumble";

struct MumbleStringWrapper wrapper;
wrapper.data = name;
wrapper.size = strlen(name);
wrapper.needsReleasing = false;

return wrapper;
}
```

If you want to read details about why a `MumbleStringWrapper` is required, have a look at the [resource management docs](ResourceManagement.md).

The implementation of `mumble_getAPIVersion` is almost trivial as long as you are sticking to the API version the headers you are using belong to
(which is strongly recommended). In that case the constant `MUMBLE_PLUGIN_API_VERSION` will hold the correct version and all you have to do is to
return it from this function:
```c
mumble_version_t mumble_getAPIVersion() {
// This constant will always hold the API version that fits the included header files
return MUMBLE_PLUGIN_API_VERSION;
}
```

The function for receiving the Mumble-API function is implemented as follows:
```c
void mumble_registerAPIFunctions(void *apiStruct) {
// Provided mumble_getAPIVersion returns MUMBLE_PLUGIN_API_VERSION, this cast will make sure
// that the passed pointer will be cast to the proper type
mumbleAPI = MUMBLE_API_CAST(apiStruct);
}
```
Note that the function takes a `void *` and thus has to cast this pointer to the correct type itself. In the case that you are using the API version
corresponding to the included headers (again: as you should), this is easy thanks to the pre-defined macro `MUMBLE_API_CAST`. It will automatically
cast the pointer to the correct API type.
The final function that needs to be implemented is `mumble_releaseResource`. Note that because our `MumbleStringWrapper` used above specifies
`needsReleasing = false`, this function will never actually be called (unless you implement other functions that do return resources that need
releasing - see [Resource management](ResourceManagement.md)) and therefore a dummy implementation is enough for our purposes:
```c
void mumble_releaseResource(const void *pointer) {
// As we never pass a resource to Mumble that needs releasing, this function should never
// get called
printf("Called mumble_releaseResource but expected that this never gets called -> Aborting");
abort();
}
```

And that's it. This is all that is strictly required in order to get a working plugin.

Note however that you will probably also want to implement the following functions (though from a functional point of view that is completely
optional):
- `mumble_getVersion`
- `mumble_getAuthor`
- `mumble_getDescription`

All available functions are listed and documented in the [plugin-API headers](PluginAPI.md#header-files).
17 changes: 17 additions & 0 deletions docs/dev/plugins/Debugging.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Debugging a Mumble plugin

Generally you have to resort to the standard debugging techniques when it comes to debugging the functionality inside your plugin. If however you want
to find out why Mumble doesn't recognize any given function in your plugin or you think that the issue may lie within Mumble somewhere, you have to
[build Mumble](../build-instructions/README.md) yourself.

When doing so, you activate the `plugin-debug` and/or `plugin-callback-debug` options when invoking cmake:
```bash
cmake -Dplugin-debug=ON -Dplugin-callback-debug=ON ..
```

The first option (`plugin-debug`) will cause Mumble to be verbose about the function resolution process in plugins. That means it will report which
functions of the plugin interface Mumble recognizes as implemented in any given plugin.

The second option will cause Mumble to print a log message to the console every time it encounters an event that is passed to interested plugins via
the respective callback function. Note that this output can be excessively verbose due to the audio-related events.

15 changes: 15 additions & 0 deletions docs/dev/plugins/LanguageBindings.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Mumble plugin - Language bindings

## C

`C` is the native language of the plugin API. If you intend to use it, you don't need any special bindings.


## C++

There exists an official `C++` wrapper around the C API. It can be found at https://github.com/mumble-voip/mumble-plugin-cpp.

## Rust

`Rust` bindings are maintained independently from the main Mumble project. They can be found at https://crates.io/crates/mumble-sys.

103 changes: 103 additions & 0 deletions docs/dev/plugins/MumbleAPI.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# The Mumble-API

The Mumble-API is a set of function pointers that is given to the plugin during the [initialization process](PluginLifecycle.md#initialization) via
the `mumble_registerAPIFunctions` function in the [plugin-API](PluginAPI.md).


## How it works

As a plugin developer you don't have to worry about the implementation of these functions. They are implemented on Mumble's side. All you have to care
about is that certain functions exist and that you are free to call any of them.

In order to do so, you need to know the function signature which is defined in the Mumble-API header file. Functions in there are defined as e.g.
```cpp
mumble_error_t(PLUGIN_CALLING_CONVENTION *isConnectionSynchronized)(mumble_plugin_id_t callerID, mumble_connection_t connection, bool *synchronized);
```
The definition may seem a bit odd and intimidating at first glance but that is only because these functions need to be specified as [function
pointers](https://www.geeksforgeeks.org/function-pointer-in-c/). Here are a set of easy steps to (mentally) convert this notation into a (probably)
more familiar format:
1. Ignore `PLUGIN_CALLING_CONVENTION`. The [calling convention](https://en.wikipedia.org/wiki/Calling_convention) is nothing you have to worry about.
This is handled for you by your compiler
2. Remove the first set of parenthesis and the star within them. What remains is the function name

Applying these steps to the above function yields the following definition:
```cpp
mumble_error_t isConnectionSynchronized(mumble_plugin_id_t callerID, mumble_connection_t connection, bool *synchronized);
```
Thus we can see that this is a function called `isConnectionSynchronized` that takes 3 parameters and returns a `mumble_error_t`. Therefore if we
assume that you have stored the Mumble-API in a variable named `mumbleAPI` in your plugin, you could call this function as
```cpp
mumble_error_t returnedError = mumbleAPI.isConnectionSynchronized(...);
```

## General function structure

Each function follows the general signature
```cpp
mumble_error_t myFunction(mumble_plugin_id_t callerID /* potentially more arguments */);
```
That means that each function _always_ returns an error code that indicates whether the API call was successful. How to handle this returned error
code is described in the [error handling section](#error-handling).
Furthermore the first parameter is _always_ the ID of the plugin that makes the API call. Your plugin's ID is given to you during the [initialization
process](PluginLifecycle.md#initialization) via the `mumble_init` function. You have to store this ID and use it every time you make a call to the
Mumble-API as otherwise your calls will be ignored by Mumble.
Given that the return value of Mumble-API functions are always occupied with the error code, all functions that query some sort of information use an
_out parameter_. That means that you pre-allocate a variable of the respective type and then pass a pointer to that variable to the API function. On
successful execution this function will then set the value of your variable through the given pointer.
A good example is obtaining the currently active server connection:
```cpp
mumble_connection_t activeConnection;
if (mumbleAPI.getActiveServerConnection(pluginID, &activeConnection) == MUMBLE_STATUS_OK) {
// Do something with activeConnection
}
```


## Error handling

The error code returned by Mumble-API functions can (and often must) be used to determine whether the given function call was successful. If the error
code compares equal to `MUMBLE_STATUS_OK` (a macro defined in the `PluginComponents` header that is automatically included in the Mumble-API header),
then the API call was successful. **Every other value indicates failure**.

In case of an error, the error code will give information on the exact problem that was found. The possible error codes are defined in the
`Mumble_ErrorCode` enum (also defined in `PluginComponents`). These values can be used to check the error code against certain expected errors in the
error-handling branch of your code.

If you want to log the error in some way, it is recommended to use the `errorMessage` function (yet again defined in the `PluginComponents` header).
This function will return a String-representation of the respective error.

Given that errors are only reported in form of error codes, it is essential to always explicitly check for them. This is especially important when
these API function are used to query information. Accessing the variable passed as an out-parameter to a Mumble-API function may lead to undefined
behavior unless you have initialized that variable to a defined value before passing it to the function.

The only guarantee about variables given as out-parameters to API functions in case of an error is that they remain unchanged by the function.


## Multithreading

All API functions are synchronized. That means they can be called from an arbitrary thread. In order to achieve this synchronization, the functions
are executed in the main thread on Mumble's side. As the functions return a value, the caller has to wait for the execution to have finished in order
to continue. In other words: API function calls are **blocking**.

Therefore special care is to be taken as this can easily lead to deadlocks as soon as the plugin spawns a custom thread and uses the API functions
from it. An example of an easy way to run into such a deadlock is as follows:

Assume the plugin has spawned a worker thread that performs some kind of work asynchronously. Let's call that thread "W". Now an event occurs that
causes a event callback to be called in the plugin-API which usually happens in the already mentioned main thread (see also
[Threading](PluginAPI.md#threading)). Inside this event callback, the plugin notifies W to perform some sort of work and waits for that to finish. For
performing that work W needs to call an API function which blocks until the function can be executed in the main thread. Given that the main thread is
currently blocked by the plugin callback, a deadlock occurs.

That is to say: You must not wait for a job to finish asynchronously that might call an API function from the plugin-API.


## Header files

The following header files describe the Mumble-API struct that contains the function pointers as well as descriptions of the respective functions:
- [Mumble-API v1.0.x](https://github.com/mumble-voip/mumble/blob/master/plugins/MumbleAPI_v_1_0_x.h)

Loading

0 comments on commit 54012c5

Please sign in to comment.