X-Plane Plugin Boilerplate

Posted by daemotron on Wed, Jan 4, 2023

X-Plane has a well-documented, accessible API, making it relatively easy to write plugins for the simulator. In this post I’m going to demonstrate how a basic plugin boilerplate can look like, and how to build it on different platforms.

Let’s get started with some basics: X-Plane plugins are dynamically linked libraries, which are loaded by X-Plane at runtime. They have to provide an API as defined by the X-Plane SDK, so X-Plane can access the plugin via defined entry points. The API documentation explains all this, and also provides examples for basic and more advanced plugins. For this post I am going to use a very barebone plugin which does nothing except existing in X-Plane.

The Furious Five

Any X-Plane plugin has to implement these five callbacks — together they constitute the most basic API any plugin must implement:

  • XPluginStart is called to initialize the plugin (e.g. when X-Plane starts)
  • XPluginStop is called when the plugin is stopped (e.g. when X-Plane shuts down)
  • XPluginEnable is called whenever the plugin gets enabled (e.g. via the plugin admin, or after having initialized the plugin)
  • XPluginDisable is called whenever the plugin gets disabled (e.g. when X-Plane’s configuration menu is opened)
  • XPluginReceiveMessage is called when a message is sent to the plugin — either by X-Plane itself, or by a third-party plugin

A plugin can have and register more callbacks, but these five are the mandatory minimum. An absolute barebone plugin could therefore look like this:

 1#include <CHeaders/XPLM/XPLMPlugin.h>
 2
 3
 4PLUGIN_API int XPluginStart(char *outName, char *outSig, char *outDesc)
 5{
 6    strcpy(outName, "DaemoPlug");
 7    strcpy(outSig, "net.daemotron.plugin");
 8    strcpy(outDesc, "The uber cool plugin that does absolutely nothing.");
 9    return 1;
10}
11
12
13PLUGIN_API void XPluginStop(void)
14{
15
16}
17
18PLUGIN_API void XPluginDisable(void)
19{
20
21}
22
23
24PLUGIN_API int XPluginEnable(void)
25{
26    return 1;
27}
28
29
30PLUGIN_API void XPluginReceiveMessage(XPLMPluginID inFrom, int inMsg, void *inParam)
31{
32 
33}

Of course this code could be written more compactly, but in a fully working plugin, all these API functions will have to be implemented in order to make the plugin actually do something.

Organizing Files

There is no best practice on how to organise files of a plugin project. Here I’m just showing you how I do organise things, since it fits well with my way of working. I use git for source code management, and I keep each plugin in its dedicated git repository. For building my plugins, I use CMake. A typical plugin boilerplate using these tools looks like this:

 1+--+ Project Root Directory
 2   |
 3   +--+ SDK/
 4   |
 5   +--+ src/
 6   |  |
 7   |  +-- collisions
 8   |  |
 9   |  +-- main.c
10   |
11   +-- .gitignore
12   |
13   +-- CMakeLists.txt
  • The SDK folder contains the X-Plane SDK as downloaded from Laminar’s Developer Site.
  • The src folder contains my source code files. I usually call the main file main.c, where the Furious Five are implemented.
  • The file src/collisions holds symbol names the linker needs to know about.
  • In the .gitignore file, I exclude the SDK and the build directory from git, and eventual folders created by an IDE or editor
  • The CMkaeLists.txt file contains the build instructions for the plugin

The content of the src/collisions linker script looks like this:

 1{
 2 global:
 3 XPluginStart;
 4 XPluginStop;
 5 XPluginEnable;
 6 XPluginDisable;
 7 XPluginReceiveMessage;
 8 local:
 9 *;
10};

Building The Plugin On Windows

With ~60%, Windows is the most-used operating system among X-Plane users1, so let’s start with building the plugin for this platform. Again there are various options which compilers could be used. I picked the MinGW-w64 distribution provided by WinLibs. On my system, I have both distributions, the Win64 and Win32 variant. The Win32 variant is only needed when building plugins for older versions of X-Plane (prior to 10.20). In this example I’m going to focus on the 64 bit variant only — X-Plane 11 and newer only work with 64 bit plugins.

For this example, I used the GCC 12.2.0 + LLVM/Clang/LLD/LLDB 15.0.6 + MinGW-w64 10.0.0 (UCRT) - release 3 package in Win64 flavour. Installing is straightforward: simply download the package and extract it with 7zip. I usually keep my MinGW installations directly on C:\, so this one lives at C:\mingw64 on my system. To make the tools accessible on the command line, you’ll need to add C:\mingw64\bin to your PATH environment variable. That’s it already, installation done.

CMake comes with its own installer, so setting it up on Windows is easy and straight forward. In addition I use cmder as console emulator on Windows — IMO much more comfortable than the default command line, but of course the normal command line will do as well.

Before we can build the plugin, we will need to create some build instructions, helping CMake understand what we expect it to do. Here’s a simple CMakeLists.txt file I used to compile the example plugin:

 1cmake_minimum_required (VERSION 3.2)
 2
 3set(CMAKE_C_COMPILER "gcc")
 4set(CMAKE_CXX_COMPILER "g++")
 5
 6project (DaemoPlug)
 7
 8# Detect SDK
 9get_filename_component(SDK_PATH "./SDK/" ABSOLUTE)
10message("-- Detecting X-Plane SDK path")
11
12if(NOT EXISTS ${SDK_PATH})
13    message(FATAL_ERROR "Missing SDK folder: ${SDK_PATH}")
14endif(NOT EXISTS ${SDK_PATH})
15
16# Copy symbol collisions file
17configure_file("src/collisions" "collisions" COPYONLY)
18
19# Global compiler flags
20set (GCC_DEFAULT_FLAGS -std=c17 -Wall -Wunreachable-code -pipe -Wextra -Wshadow -Wfloat-equal -pedantic -fvisibility=hidden -O2 -fmessage-length=0 -D_FORTIFY_SOURCE=2 -fstack-protector -funwind-tables -fasynchronous-unwind-tables -W -DXPLM200 -DXPLM210)
21set (GXX_DEFAULT_FLAGS -std=c++17 -Wall -Wunreachable-code -pipe -Wextra -Wshadow -Wfloat-equal -pedantic -fvisibility=hidden -O2 -fmessage-length=0 -D_FORTIFY_SOURCE=2 -fstack-protector -funwind-tables -fasynchronous-unwind-tables -W -DXPLM200 -DXPLM210)
22
23# Include files
24include_directories ("./src")
25include_directories ("${SDK_PATH}")
26include_directories ("${SDK_PATH}/CHeaders/XPLM")
27include_directories ("${SDK_PATH}/CHeaders/Wrappers")
28include_directories ("${SDK_PATH}/CHeaders/Widgets")
29
30file(GLOB_RECURSE SOURCES "src/*.c")
31
32if(WIN32)
33    cmake_policy(SET CMP0015 NEW)
34    set (CMAKE_C_COMPILER x86_64-w64-mingw32-gcc)
35    set (CMAKE_CXX_COMPILER x86_64-w64-mingw32-g++)
36    link_directories("${SDK_PATH}/Libraries/Win")
37    add_library(WIN_RELEASE_64 SHARED ${SOURCES})
38    target_compile_options(WIN_RELEASE_64 PRIVATE ${GCC_DEFAULT_FLAGS} -s -c -fno-stack-protector -static-libgcc -static-libstdc++ -DXPLM200 -DIBM)
39    target_link_libraries(WIN_RELEASE_64 XPLM_64 XPWidgets_64 -static-libgcc -static-libstdc++)
40    set_target_properties(WIN_RELEASE_64 PROPERTIES LINK_FLAGS "-s -Wl,--version-script=collisions")
41    set_target_properties(WIN_RELEASE_64 PROPERTIES PREFIX "")
42    set_target_properties(WIN_RELEASE_64 PROPERTIES SUFFIX "")
43    set_target_properties(WIN_RELEASE_64 PROPERTIES LIBRARY_OUTPUT_DIRECTORY "DaemoPlug/64/")
44    set_target_properties(WIN_RELEASE_64 PROPERTIES OUTPUT_NAME "win.xpl")
45endif()

This CMake file instructs CMake to

  1. use gcc as compiler
  2. detect the SDK path
  3. define global compiler flags, including the X-Plane API versions we want to use (-DXPLM200)
  4. define include directories including our source folder and the SDK headers
  5. consider all *.c files in src/ as source files
  6. explicitly use the 64 bit mingw compilers on Windows
  7. build a shared library linked against the Windows-specific libraries in the SDK, and name it win.xpl

You can amend all this to your needs — you can add the XPLM300, XPLM301 and XPLM303 APIs if you want to address only X-Plane 11.50 and newer, or use the Fat Plugins (XPLM 3.0 API) format instead of the Fat Plugins (XPLM 2.1 API) format used in the example above.

To build the plugin, issue these instructions on a command line in the project folder:

1mkdir build
2cd build
3cmake -G "MinGW Makefiles" ../
4mingw32-make

Does It Work?

Now we have build our plugin, it’s time to see if it actually loads in X-Plane. in X-Plane/Resources/plugins create a folder DaemoPlug (or whatever you’d like to call it), and a subfolder 64. Copy the freshly created win.xpl file into that subfolder and launch X-Plane. In X-Plane’s log file, you should find a line similar to this one:

1Loaded: D:\X-Plane 12/Resources/plugins/DaemoPlug/64/win.xpl (net.daemotron.plugin).

Also in the Plugin Admin window X-Plane will show you the plugin:

X-Plane Plugin Admin

Building on Mac and Linux

To build the plugin on Mac or Linux, the respective build tools have to be installed. For Mac, this would be Xcode (or at least the Xcode command line tools), and on Linux, at least GCC and GNU make would be required (Ubuntu provides all required tools in the build-essential package).

Also currently our CMakeLists.txt only covers building on Windows. Adding these parts to the bottom of the file will enable builds on Linux and Mac:

 1if(LINUX)
 2    add_library(LIN_RELEASE_64 SHARED ${SOURCES})
 3    target_compile_options(LIN_RELEASE_64 PRIVATE ${GCC_DEFAULT_FLAGS} -m64 -fPIC -DLIN)
 4    set_target_properties(LIN_RELEASE_64 PROPERTIES LINK_FLAGS "-Wl,--version-script=collisions")
 5    set_target_properties(LIN_RELEASE_64 PROPERTIES PREFIX "")
 6    set_target_properties(LIN_RELEASE_64 PROPERTIES SUFFIX "")
 7    set_target_properties(LIN_RELEASE_64 PROPERTIES LIBRARY_OUTPUT_DIRECTORY "DaemoPlug/64/")
 8    set_target_properties(LIN_RELEASE_64 PROPERTIES OUTPUT_NAME "lin.xpl")
 9endif()
10
11if(APPLE)
12    cmake_policy(SET CMP0042 NEW)
13    set (CMAKE_SYSTEM_NAME Darwin)
14    set (CMAKE_CXX_COMPILER g++)
15    set (CMAKE_OSX_ARCHITECTURES "x86_64;arm64")
16    add_library(MAC_RELEASE_64 SHARED ${SOURCES})
17    target_compile_options(MAC_RELEASE_64 PRIVATE ${GCC_DEFAULT_FLAGS} -nostdinc++ -I/Library/Developer/CommandLineTools/usr/include/c++/v1 -DXPLM200 -DAPL)
18    target_link_libraries(MAC_RELEASE_64 "-F${SDK_PATH}/Libraries/Mac" "-framework XPLM" "-framework XPWidgets" "-framework OpenGL" "-nodefaultlibs" "-lc++" "-lc++abi" "-lm" "-lc")
19    set_target_properties(MAC_RELEASE_64 PROPERTIES LINK_FLAGS "-m64 -fvisibility=hidden")
20    set_target_properties(MAC_RELEASE_64 PROPERTIES PREFIX "")
21    set_target_properties(MAC_RELEASE_64 PROPERTIES SUFFIX "")
22    set_target_properties(MAC_RELEASE_64 PROPERTIES LIBRARY_OUTPUT_DIRECTORY "DaemoPlug/64/")
23    set_target_properties(MAC_RELEASE_64 PROPERTIES OUTPUT_NAME "mac.xpl")
24endif()

Building the plugin on these systems is similar to Windows, with just one difference — they use (g)make instead of mingw32-make:

1mkdir build
2cd build
3cmake ../
4make