Writing Language Module
How to add support for a new language to Plugify with a language module.
A language module is a native library that teaches the core one language: it loads plugins written in it, starts and stops them, and connects their methods with the methods of plugins in other languages. This page describes what a module has to do and how to start one.
Quick start
Clone the template with its submodules
Build it
Fill in the module
Implement the methods in src/module.cpp and set the language in plugify-module-template.pmodule.in.
The template has the core as a submodule in external/plugify, the module class with empty methods, the manifest template, export lists for the linker in sym/, a conda recipe in conda/ and a GitHub workflow that builds releases. The existing modules (C++, Python, Lua and others) are full examples.
The module manifest
A module is found by its *.pmodule manifest:
language is the key that connects plugins to the module: a plugin with "language": "demo" is loaded by this module, and the core makes it depend on the module. The library of the module is bin/ plus the name of the manifest file by default, here bin/libdemo_module.so on Linux and bin/demo_module.dll on Windows. Set runtime to use another path, and directories to add folders where the library's own dependencies are searched. All fields are described in Architecture.
Entry point
The library must export one C function. The core calls it once after loading the library:
PLUGIFY_MODULE_API stands for the export macro of your library; in the template it is LM_EXPORT. Return a pointer to an object that lives as long as the library, for example a global.
The ILanguageModule interface
All methods except IsDebugBuild return Result, which is std::expected<T, std::string>. To report a failure, return plugify::MakeError("text {}", args...); the core marks the extension as failed with that text. Exceptions thrown from these methods are caught by the core and treated the same way.
| Method | When the core calls it |
|---|---|
Initialize | Once, after loading the module. Keep the Provider: it gives you logging, the folders of the host and the list of extensions |
OnPluginLoad | For each plugin of your language, in load order. Load the plugin and return its methods |
OnMethodExport | For each plugin that has methods, after all plugins are loaded. Called for every module, whatever the language of the plugin |
OnPluginStart | For each plugin of your language, after all exports |
OnUpdate, OnPluginUpdate | From the host's Update, if the matching flag in MethodTable is set |
OnPluginEnd | For each plugin of your language on shutdown, in reverse load order |
Shutdown | Once, when the module is unloaded |
IsDebugBuild | Must return true for a debug build. On Windows the core refuses a module built in another configuration than itself |
MethodTable
MethodTable tells the core which optional calls exist, so it does not call empty ones:
| Field | Returned in | Meaning |
|---|---|---|
hasUpdate | InitData | The module wants OnUpdate |
hasUpdate | LoadData | The plugin wants OnPluginUpdate |
hasStart, hasEnd | LoadData | The plugin wants OnPluginStart, OnPluginEnd |
hasExport | LoadData | The plugin has methods, so OnMethodExport is called for it |
What OnPluginLoad returns
methods: for each entry ofplugin.GetMethods(), in the same order, aMethodDatawith a reference to thatMethodand the native address of its implementation.funcNameof the method tells you where to find the implementation. If the list does not match the manifest or an address is null, the core rejects the plugin.data: any pointer you want to keep for this plugin. The core gives it back asplugin.GetUserData().table: the flags described above.
The plugin's files are in plugin.GetLocation(), and its entry from the manifest is plugin.GetEntry().
Connecting languages
Every method of every plugin is a native function with the C calling convention of the platform. A module works in two directions:
- Exporting. In
OnPluginLoadthe module gives the core a native address for each method of its plugin. A native language can give the address of the function itself. Other languages need a native function that converts the arguments and calls the plugin code;JitCallbackgenerates one at run time. - Importing. In
OnMethodExport(plugin)the module receives a plugin whose methods are inplugin.GetMethodsData(), and makes them callable from its own plugins. A native language can call the address directly. Other languages call it through a function generated byJitCall, after converting their values to Plugify types.
The existing modules import methods in the way that is natural for their language:
| Module | How plugin P sees method M of another plugin |
|---|---|
| C++, Rust, Go | A function pointer variable __P_M in the plugin library, filled in by the module |
| C# (.NET) | A generated class P.P with the method M |
| Python | Module plugify.pps.P |
| JavaScript | ES module :P |
| Lua | Table P |
All plugins are loaded and exported before the first one is started, so in OnPluginStart all imports are filled in.
A minimal module
This module serves the language demo. For each method of a plugin it generates a native function that adds two int32 values, and when the plugin starts it calls each method through JitCall:
With a plugin that declares a method Add(int32 a, int32 b) -> int32 and "language": "demo", plug prints demo_plugin.Add(2, 3) = 5.
Testing
- Put the module and a few plugins into the
extensionsfolder ofplugand check that they reachRunning. - Test calls between languages with
cross_call_masterandcross_call_workerfrom the C++ module repository: write a worker in your language with the same methods as the C++ one, load it together with the master and the C++ module, and the master checks every kind of parameter. See Build Instructions. - On Windows, test both debug and release builds against a core of the same configuration.
Publishing
Modules are distributed as conda packages, the same way as plugins. The template's workflow uses release-please: on a release it builds the module for Linux and Windows, makes conda packages from the recipe in conda/ and attaches them to the GitHub release. The package must put the manifest in the root of the environment and the library in bin/, the same layout the core expects in extensions.