Writing Language Module

Guide for extending Plugify with new languages.

This guide will walk you through the process of creating your first language module for the Plugify system. A language module allows Plugify to support plugins written in a specific programming language. By following this guide, you'll learn how to define a configuration file, implement the required interface, and integrate your module with the Plugify core.

Quick Start

Clone the template project:

git clone https://github.com/untrustedmodders/template-lang-module.git

Build the project:

mkdir build && cd build
cmake --preset Debug
cmake --build .

Implement the ILanguageModule interface.

Introduction

A language module in Plugify is a dynamic library that provides support for a specific programming language. Each language module must include a configuration file named .pmodule, which contains metadata and settings required by the Plugify core to load and manage the module.

The Module Manifest

The .pmodule file is a JSON configuration file that defines essential information about your language module. Below is an example of a .pmodule file:

*.pmodule
{
    "fileVersion": 1,
    "version": "1.0.0",
    "friendlyName": "C++ Language Module",
    "language": "cpp",
    "description": "Adds support for C++ plugins",
    "createdBy": "untrustedmodders",
    "createdByURL": "https://github.com/untrustedmodders/",
    "docsURL": "https://github.com/untrustedmodders/cpp-lang-module/README.md",
    "downloadURL": "https://github.com/untrustedmodders/cpp-lang-module/releases/download/v1.0/cpp-lang-module.zip",
    "updateURL": "https://raw.githubusercontent.com/untrustedmodders/cpp-lang-module/main/cpp-lang-module.json",
    "supportedPlatforms": [],
    "forceLoad": false
}

Explanation of Configuration Options

  • fileVersion: The version of the configuration file format.
  • version: The semantic version of the language module.
  • friendlyName: A user-friendly name for the language module.
  • language: The programming language supported by the module (e.g., "cpp" for C++).
  • description: A brief description of the module's functionality.
  • createdBy: The author or organization that created the module.
  • createdByURL: A URL linking to the creator's profile or website.
  • docsURL: A URL to the module's documentation.
  • downloadURL: The URL for downloading the module (e.g., a ZIP file).
  • updateURL: The URL for checking and fetching updates.
  • supportedPlatforms: A list of platforms supported by the module (e.g., Windows, Linux).
  • forceLoad: If true, the module will be forcibly loaded by the Plugify core.

Implementing the ILanguageModule Interface

The ILanguageModule interface defines the methods that your language module must implement. Below is an overview of the interface:

plugin.cpp
namespace plugify {
    class ILanguageModule {
    protected:
    ~ILanguageModule() = default;

    public:
        virtual InitResult Initialize(std::weak_ptr<IPlugifyProvider> provider, ModuleHandle module) = 0;
        virtual void Shutdown() = 0;
        virtual void OnUpdate(DateTime dt) = 0;
        virtual LoadResult OnPluginLoad(PluginHandle plugin) = 0;
        virtual void OnPluginStart(PluginHandle plugin) = 0;
        virtual void OnPluginUpdate(PluginHandle plugin, DateTime dt) = 0;
        virtual void OnPluginEnd(PluginHandle plugin) = 0;
        virtual void OnMethodExport(PluginHandle plugin) = 0;
        virtual bool IsDebugBuild() = 0;
    };
}

Key Methods

  • Initialize: Called when the module is loaded. Use this to set up your module.
  • Shutdown: Called when the module is unloaded. Use this to clean up resources.
  • OnUpdate: Called when the module is updated. Use this for periodic updates.
  • OnPluginLoad: Called when a plugin is loaded. Use this to initialize the plugin.
  • OnPluginStart: Called when a plugin is started.
  • OnPluginUpdate: Called when a plugin is updated.
  • OnPluginEnd: Called when a plugin is ended.
  • OnMethodExport: Called when a plugin exports methods for inter-plugin communication.
  • IsDebugBuild: Returns true if the module is built in debug mode.

Steps to Create Your First Language Module

Set Up Your Development Environment

Ensure you have the following tools installed:

  • A text editor or IDE (e.g., Visual Studio, CLion).
  • A compatible C++ compiler (e.g., MSVC, GCC, Clang).
  • The Plugify framework installed and configured.

Use the Template Project

To simplify the process, clone the template-lang-module repository:

git clone https://github.com/untrustedmodders/template-lang-module.git

Configure and Build the Template

  1. Navigate to the project directory:
    cd template-lang-module
    
  2. Generate build files:
    mkdir build && cd build
    cmake --preset Debug
    
  3. Build the project:
    cmake --build .
    

Define Your Module's Functionality

  • Implement the ILanguageModule interface in your module.
  • Add logic to load, manage, and execute plugins in your target language.
  • Use the plugify-function library to dynamically generate C functions if needed.

Implement Marshaling Functionality (if needed)

For languages that require type conversion, implement marshaling wrappers to convert Plugify types (e.g., plg::vector, plg::string) to native types.

Create the Module Manifest

The .pmodule file is a JSON configuration file that defines essential information about your language module. The template project already includes a .pmodule file, so you only need to modify it to suit your module.

Key Fields to Update

  • language: Specify the programming language that your module supports (e.g., cpp, python, javascript).
  • friendlyName: Provide a user-friendly name for your module.
  • description: Add a brief description of your module's functionality.
  • downloadURL: Update this field to point to the URL where your module's ZIP archive will be hosted.

Example of Manifest File

Here’s an example of a .pmodule file for a C++ language module:

*.pmodule
{
    "fileVersion": 1,
    "version": "1.0.0",
    "friendlyName": "C++ Language Module",
    "language": "cpp",
    "description": "Adds support for C++ plugins",
    "createdBy": "untrustedmodders",
    "createdByURL": "https://github.com/untrustedmodders/",
    "docsURL": "https://github.com/untrustedmodders/cpp-lang-module/README.md",
    "downloadURL": "https://github.com/untrustedmodders/cpp-lang-module/releases/download/v1.0/cpp-lang-module.zip",
    "updateURL": "https://raw.githubusercontent.com/untrustedmodders/cpp-lang-module/main/cpp-lang-module.json",
    "supportedPlatforms": [],
    "forceLoad": false
}

How to Modify the Template

  1. Open the .pmodule file in your template project.
  2. Update the language field to specify the programming language your module supports.
  3. Modify the friendlyName, description, and other fields as needed.
  4. Ensure the downloadURL points to the location where your module's ZIP archive will be hosted.

Package Your Module

Package your module files and the .pmodule file into a ZIP archive. Update the downloadURL in your .pmodule file to point to this archive.

File Structure for the ZIP Archive

The ZIP archive for the package manager should contain the following files in this structure:

  • bin/
    • template_language_module.dll
    • libtemplate_language_module.so
  • template_language_module.pplugin
  • bin/: This directory should contain the compiled binaries for your language module (e.g., .dll for Windows and .so for Linux).
  • template_language_module.pplugin: This is the plugin manifest file that describes your language module.

Test Your Module

To properly test your language module, you need to create a cross_call_worker and use it together with the cross_call_master to verify that all functions are marshaled correctly. This ensures that your module can handle inter-plugin communication and data exchange.

Testing with Cross-Plugin Communication

  1. Create a cross_call_worker: Implement a plugin that exports methods to be called by the cross_call_master. This plugin should handle data exchange and method calls.
  2. Use the cross_call_master: The cross_call_master will call methods from your cross_call_worker and verify that the data is correctly marshaled and processed.
  3. Check the C++ Example: For a working example, refer to the cpp-language-module repository, which includes a fully implemented cross_call_worker and cross_call_master for testing.

Example Workflow

  1. The cross_call_master calls a method from the cross_call_worker to pass a string.
  2. The cross_call_worker processes the string and calls a method back on the cross_call_master to return a modified string.
  3. Verify that the string is correctly passed and returned.

Publish Your Module

Upload your module package to a hosting service (e.g., GitHub Releases) and share it with the Plugify community. To streamline the release process, you can use GitHub Actions to automatically create releases, generate repository JSON files, and handle platform-specific builds.

Using GitHub Actions for Releases

Our team has implemented GitHub Actions workflows in existing language modules (e.g., cpp-language-module) to automate the release process. These workflows can serve as a good example for setting up your own automated release pipeline.

Key Features of the Workflow
  1. Automatic Releases:
    • When a new tag is pushed (e.g., v1.0.0), the workflow automatically creates a GitHub release.
    • The release includes the compiled module files and the .pmodule manifest.
  2. Repository JSON Generation:
    • The workflow generates a repository JSON file (e.g., plugify-module-cpp.json) that contains metadata about the release.
    • This file is used by the Plugify package manager to check for updates.
  3. Platform-Specific Builds:
    • The workflow builds the module for multiple platforms (e.g., Windows, Linux) and includes the platform-specific binaries in the release.
  4. Checksum Verification:
    • The workflow calculates checksums for the release artifacts to ensure file integrity.
  5. GitHub Pages Deployment:
    • The repository JSON file is deployed to GitHub Pages, making it accessible to the Plugify package manager.
  6. Discord Notifications:
    • The workflow sends a notification to a Discord channel when a new release is published.
Example Workflow

Here’s an example of a GitHub Actions workflow for automating releases:

How to Use
  1. Copy the workflow file (e.g., .github/workflows/release.yml) from one of our existing language modules (e.g., cpp-language-module).
  2. Modify the workflow to match your module's build process and file structure.
  3. Push the workflow file to your repository and create a new tag (e.g., v1.0.0) to trigger the release process.

By using GitHub Actions, you can automate the release process and ensure that your module is always up-to-date and easily accessible to users.

Maintain and Update Your Module

  • Update the version, and other fields in your .pmodule file for each release.
  • Ensure the updateURL points to the latest .json file for automatic updates.

Best Practices

  • Keep Dependencies Minimal: Avoid unnecessary dependencies to ensure compatibility and performance.
  • Use Debug Builds for Testing: Test your module in debug mode to catch issues early.
  • Document Your Module: Provide clear documentation for users and developers.
  • Follow Semantic Versioning: Use semantic versioning (e.g., 1.0.0) for your module releases.

Troubleshooting

  • Module Not Loading: Ensure the .pmodule file is correctly formatted and placed in the right directory.
  • Plugins Failing to Initialize: Check the logs for errors and verify that your module implements all required methods.
  • Debugging Tips: Use verbose logging and a debugger to identify and fix issues.