Export Functions

Guide to export functions from your C++ plugin to be used by other language modules within Plugify.

In the Plugify ecosystem, C++ plugins can export functions to make them accessible to other plugins. This guide explains how to export functions in C++ and provides examples to help you integrate your plugins seamlessly.

Basic Type Mapping

The following table lists how types are exposed to the C++ API:

C++ TypePlugify AliasRef Support ?
voidvoid❌
boolbool✅
charchar8✅
char16_tchar16✅
int8_tint8✅
int16_tint16✅
int32_tint32✅
int64_tint64✅
uint8_tuint8✅
uint16_tuint16✅
uint32_tuint32✅
uint64_tuint64✅
uintptr_tptr64✅
uintptr_tptr32✅
floatfloat✅
doubledouble✅
void*function❌
plg::stringstring✅
plg::anyany✅
plg::vector<bool>bool[]✅
plg::vector<char>char8[]✅
plg::vector<char16_t>char16[]✅
plg::vector<int8_t>int8[]✅
plg::vector<int16_t>int16[]✅
plg::vector<int32_t>int32[]✅
plg::vector<int64_t>int64[]✅
plg::vector<uint8_t>uint8[]✅
plg::vector<uint16_t>uint16[]✅
plg::vector<uint32_t>uint32[]✅
plg::vector<uint64_t>uint64[]✅
plg::vector<uintptr_t>ptr64[]✅
plg::vector<uintptr_t>ptr32[]✅
plg::vector<float>float[]✅
plg::vector<double>double[]✅
plg::vector<plg::string>string[]✅
plg::vector<plg::any>any[]✅
plg::vector<plg::vec2>vec2[]✅
plg::vector<plg::vec3>vec3[]✅
plg::vector<plg::vec4>vec4[]✅
plg::vector<plg::mat4x4>mat4x4[]✅
plg::vec2vec2✅
plg::vec3vec3✅
plg::vec4vec4✅
plg::mat4x4mat4x4✅

Exporting Functions in C++

To export a function in a C++ plugin, you need to ensure that the function is visible to other plugins. This is typically done by marking the function with the PLUGIN_API macro, which ensures the function is exported when the plugin is compiled as a dynamic link library (DLL).

Key Points

  • C Linkage: The PLUGIN_API macro does not automatically include extern "C" to prevent name mangling. We need ensure the function can be found just by name by adding extern "C".
  • Parameter and Return Types: Use Plugify's native types (e.g., plg::string, plg::vector) for seamless integration.
  • Version Script on Linux: If your build uses a version script with local: *; (the template does, in sym/version_script.lds), list every exported function under global:, otherwise the language module cannot find it.

Generating the PLUGIN_API Macro

The PLUGIN_API macro is generated using CMake. Here’s how you can set it up in your CMakeLists.txt file:

include(GenerateExportHeader)
generate_export_header(${PROJECT_NAME}
    EXPORT_MACRO_NAME PLUGIN_API
    EXPORT_FILE_NAME ${CMAKE_BINARY_DIR}/exports/plugin_export.h
)
target_include_directories(${PROJECT_NAME} PRIVATE ${CMAKE_BINARY_DIR}/exports)

This generates a header file (plugin_export.h) that defines the PLUGIN_API macro. Include this header in your plugin source files to mark functions for export.

Basic Example

Here’s a simple example of exporting a function in a C++ plugin:

Function Definition

plugin.cpp
#include <cstdint>
#include <plugin_export.h> // Include the generated export header

extern "C" PLUGIN_API int32_t AddNumbers_Exported_Name(int32_t a, int32_t b) {
    return a + b;
}

Exporting the Function

When the plugin is loaded, the function AddNumbers will be exported and can be called by other plugins.

Plugin Manifest Example

All exported functions must be described in the plugin's manifest file under the methods section. Here’s an example manifest for a plugin that exports the AddNumbers function:

plugin_name.pplugin
{
  "name": "example_plugin",
  "version": "1.0.0",
  "methods": [
    {
      "name": "AddNumbers",
      "funcName": "AddNumbers_Exported_Name",
      "paramTypes": [
        {
          "type": "int32",
          "name": "a"
        },
        {
          "type": "int32",
          "name": "b"
        }
      ],
      "retType": {
        "type": "int32"
      }
    }
  ]
}

Advanced Example: Exporting Complex Functions

Here’s an example of exporting a function with complex parameter and return types:

Function Definition

plugin.cpp
#include <string>
#include <plg/string.hpp>
#include <plg/vector.hpp>
#include <plugin_export.h>

extern "C" PLUGIN_API plg::vector<plg::string> ProcessData_Exported_Name(const plg::vector<double>& data, const plg::string& prefix) {
    plg::vector<plg::string> result;
    for (double value : data) {
        result.push_back(prefix + std::to_string(value).c_str());
    }
    return result;
}

Plugin Manifest

plugin_name.pplugin
{
  "name": "example_plugin",
  "version": "1.0.0",
  "methods": [
    {
      "name": "ProcessData",
      "funcName": "ProcessData_Exported_Name",
      "paramTypes": [
        {
          "type": "double[]",
          "name": "data"
        },
        {
          "type": "string",
          "name": "prefix"
        }
      ],
      "retType": {
        "type": "string[]"
      }
    }
  ]
}

Exporting Functions with References

Plugify supports reference parameters (also known as "out" or "ref" parameters) that let functions modify values and return them to the caller. In C++, reference parameters are declared with &.

Function Definition with Reference Parameters

plugin.cpp
#include <cstdint>
#include <plugin_export.h>

extern "C" PLUGIN_API void IncrementValue_Exported_Name(int32_t& value) {
    /**
     * Increments an integer value by 1.
     *
     * @param value The value to increment (passed by reference).
     */
    value += 1;
}

extern "C" PLUGIN_API void Calculate_Exported_Name(int32_t a, int32_t b, int32_t& sum, int32_t& product) {
    /**
     * Calculates the sum and product of two numbers, returning both via references.
     *
     * @param a First number.
     * @param b Second number.
     * @param sum Output parameter for the sum.
     * @param product Output parameter for the product.
     */
    sum = a + b;
    product = a * b;
}

Plugin Manifest with Reference Parameters

In the manifest, mark parameters passed by reference with "ref": true:

plugin_name.pplugin
{
  "name": "example_plugin",
  "version": "1.0.0",
  "methods": [
    {
      "name": "IncrementValue",
      "funcName": "IncrementValue_Exported_Name",
      "paramTypes": [
        {
          "type": "int32",
          "name": "value",
          "ref": true
        }
      ],
      "retType": {
        "type": "void"
      }
    },
    {
      "name": "Calculate",
      "funcName": "Calculate_Exported_Name",
      "paramTypes": [
        {
          "type": "int32",
          "name": "a"
        },
        {
          "type": "int32",
          "name": "b"
        },
        {
          "type": "int32",
          "name": "sum",
          "ref": true
        },
        {
          "type": "int32",
          "name": "product",
          "ref": true
        }
      ],
      "retType": {
        "type": "void"
      }
    }
  ]
}

Reference Parameter Support

Reference parameters work with most Plugify types, as shown in the "Ref Support" column of the type mapping table. The following types do not support references:

  • void (cannot be passed by reference)
  • function (callback types)

All other types, including primitives, strings, vectors and structures, support reference parameters.

Handling Callbacks

Plugify allows you to export functions that accept callbacks as parameters. Here’s an example:

Function Definition

#include <string>
#include <plg/string.hpp>
#include <plg/vector.hpp>
#include <plugin_export.h>

using CallbackFunction = plg::string(*)(int32_t, const plg::string&);

extern "C" PLUGIN_API void ExecuteWithCallback_Exported_Name(int32_t value, const plg::string& input, CallbackFunction callback) {
    plg::string result = callback(value, input);
    // Process the result
}

Plugin Manifest

plugin_name.pplugin
{
  "name": "example_plugin",
  "version": "1.0.0",
  "methods": [
    {
      "name": "ExecuteWithCallback",
      "funcName": "ExecuteWithCallback_Exported_Name",
      "paramTypes": [
        {
          "type": "int32",
          "name": "value"
        },
        {
          "type": "string",
          "name": "input"
        },
        {
          "type": "function",
          "name": "callback",
          "prototype": {
            "name": "ExampleCallback",
            "funcName": "ExampleCallback_Exported_Name",
            "paramTypes": [
              {
                "type": "int32",
                "name": "value"
              },
              {
                "type": "string",
                "name": "input"
              }
            ],
            "retType": {
              "type": "string"
            }
          }
        }
      ],
      "retType": {
        "type": "void"
      }
    }
  ]
}

Automating Manifest Generation

Instead of manually writing the manifest JSON for each exported function, you can use automated tools to extract function signatures, types, parameter descriptions, and even enum definitions directly from your C++ source code.

Benefits of Automated Generation

  1. No Manual JSON Writing: Function signatures are automatically extracted from your code
  2. Complete Type Information: Enum structures and function typedefs are included automatically
  3. Documentation Integration: Doxygen comments are parsed and included in the manifest
  4. Reduced Errors: Eliminates typos and type mismatches between code and manifest
  5. Easy Maintenance: Changes to function signatures are automatically reflected

Setup: Adding Documentation Generation to CMake

Add the following to your CMakeLists.txt to generate documentation and manifest JSON:

# Find clang-doc (usually comes with LLVM/Clang installation)
find_program(CLANG_DOC clang-doc)

if(CLANG_DOC)
    # Generate YAML documentation from your exported functions
    add_custom_target(docs
        COMMAND ${CLANG_DOC}
            --executor=all-TUs
            -p ${CMAKE_CURRENT_BINARY_DIR}
            --output=${CMAKE_CURRENT_SOURCE_DIR}/docs
            --extra-arg=-Wno-error
            --format=yaml
            ${CMAKE_SOURCE_DIR}/src/export/*.cpp  # Path to your exported functions
        WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
        COMMENT "Generating documentation with clang-doc"
    )
    
    # Optional: run the parser after the docs are generated
    find_program(PLUGIFY_PARSER parser PATHS ${CMAKE_SOURCE_DIR}/tools)
    if(PLUGIFY_PARSER)
        add_custom_command(TARGET docs POST_BUILD
            COMMAND ${PLUGIFY_PARSER}
                    ${CMAKE_SOURCE_DIR}/docs/index.yaml
                    ${CMAKE_SOURCE_DIR}/exported_methods.json
            COMMENT "Parsing documentation to JSON"
        )
    endif()
endif()

Using the Parser

Install the Parser

The parser is a Go program in the parser folder of the C++ language module. Build it with Go 1.21 or later:

go build -o parser .

A Python version with the same arguments is kept in parser/python/parser.py (needs pip install pyyaml).

Document Your Functions

Add Doxygen comments to your exported functions:

/**
 * @brief Creates a console command as an administrative command.
 * If the command does not exist, it is created. When this command is invoked,
 * the access rights of the player are automatically checked before allowing it to continue.
 * 
 * @param name The name of the console command.
 * @param adminFlags The admin flags that indicate which admin level can use this command.
 * @param description A brief description of what the command does.
 * @param flags Command flags that define the behavior of the command.
 * @param callback A callback function that is invoked when the command is executed.
 * @param mode Whether the hook was in post mode (after processing) or pre mode (before processing).
 * @return A boolean indicating whether the command was successfully added.
 */
extern "C" PLUGIN_API bool AddAdminCommand(
    const plg::string& name,
    int64_t adminFlags,
    const plg::string& description,
    ConVarFlag flags,
    CommandCallback callback,
    HookMode mode
);

Document Enums and Typedefs

Document your enums and function typedefs:

/**
 * @brief Enum representing the type of callback.
 */
enum class HookMode : uint8_t {
    /** Callback will be executed before the original function */
    Pre = 0,
    /** Callback will be executed after the original function */
    Post = 1
};

/**
 * @brief Handles the execution of a command triggered by a caller.
 * This function processes the command, interprets its context, and handles any provided arguments.
 */
using CommandCallback = ResultType (*)(int32_t caller, CommandCallingContext context, const plg::vector<plg::string>& arguments);

Generate Documentation

Build the documentation target:

cmake --build . --target docs

This generates docs/index.yaml with all your function information.

Parse to JSON

Run the parser to convert YAML to the manifest format:

./parser docs/index.yaml exported_methods.json

Or with filtering:

# Only functions from files starting with "commands"
./parser docs/index.yaml exported_methods.json --file-filter "commands"

# Only functions with "Admin" in their name
./parser docs/index.yaml exported_methods.json --name-filter "Admin"

Use in Your Manifest

Copy the generated methods array from exported_methods.json into your plugin manifest:

{
  "name": "example_plugin",
  "version": "1.0.0",
  "methods": [
    // Paste the content from exported_methods.json here
  ]
}

Advanced: Enum Structures

The parser automatically includes complete enum definitions when a parameter uses an enum type:

Generated Output:

{
  "name": "mode",
  "type": "uint8",
  "description": "Whether the hook was in post mode or pre mode.",
  "enum": {
    "name": "HookMode",
    "description": "Enum representing the type of callback.",
    "values": [
      {
        "name": "Pre",
        "value": 0,
        "description": "Callback will be executed before the original function"
      },
      {
        "name": "Post",
        "value": 1,
        "description": "Callback will be executed after the original function"
      }
    ]
  }
}

Advanced: Function Typedefs

Function pointer typedefs are automatically parsed into complete prototypes:

Generated Output:

{
  "name": "callback",
  "type": "function",
  "description": "A callback function that is invoked when the command is executed.",
  "prototype": {
    "name": "CommandCallback",
    "funcName": "CommandCallback",
    "description": "Handles the execution of a command triggered by a caller.",
    "paramTypes": [
      {
        "name": "param1",
        "type": "int32"
      },
      {
        "name": "param2",
        "type": "int32",
        "enum": {
          "name": "CommandCallingContext",
          "values": [...]
        }
      },
      {
        "name": "param3",
        "type": "string[]"
      }
    ],
    "retType": {
      "type": "int32",
      "enum": {
        "name": "ResultType",
        "values": [...]
      }
    }
  }
}

Best Practices

  1. Use PLUGIN_API: Always use the PLUGIN_API macro to mark functions for export.
  2. Follow Type Conventions: Adhere to Plugify's type conventions for parameters and return values.
  3. Document Your Functions: Use Doxygen-style comments (@brief, @param, @return) to document exported functions.
  4. Automate When Possible: Use the clang-doc parser to automate manifest generation and reduce manual errors.
  5. Document Enums and Typedefs: Include Doxygen comments on enums and typedefs for complete manifest generation.
  6. Test Thoroughly: Test your exported functions to ensure they work as expected when called by other plugins.
  7. Keep Manifests Updated: If using manual manifests, ensure they stay synchronized with your code. If using automation, regenerate after changes.

Conclusion

Exporting functions in C++ plugins is straightforward when you follow Plugify's conventions and best practices. By using the PLUGIN_API macro, adhering to type conventions, and optionally automating manifest generation with clang-doc and the Python parser, you can create robust and interoperable plugins with minimal manual effort. For more advanced use cases, such as handling callbacks or working with enums, the automated parser provides complete type information including enum structures and function prototypes.