Architecture
How it works under the hood (high-level explanation).
The core architecture of Plugify is designed to facilitate seamless interaction between language modules, each implemented as a separate C++ library. As a result, the core does not communicate directly with plugins; instead, all interactions occur through these language modules. Likewise, plugins do not directly interact with the core but rely on language modules for communication. This modular design even extends to the C++ language itself, requiring a dedicated language module for handling C++-based plugins. The primary objective of the Plugify project is to establish a universal environment that enables fast and efficient inter-language interaction.
Core Components of Plugify
Plugify consists of several key components, each fulfilling distinct roles within the system:
- Plugify Context: A class responsible for initializing and terminating the Plugify system. It provides methods for logging, accessing system components, and retrieving configuration information.
- Plugify Provider: Offers core functionality to language modules, including logging support, access to the base directory, and mechanisms for detecting and locating plugins and modules.
- Plugin Manager: Oversees the loading and management of language modules and plugins, ensuring the correct order of initialization.
- Package Manager: Manages both local and remote packages, handling tasks such as downloading, deleting, and updating components.
Package System
Packages in Plugify contain manifest files with extensions such as .pplugin
for plugins and .pmodule
for language modules. These manifest files, formatted in JSON, provide essential metadata, including exported methods, language specifications, and unique entry points for each module. The package name is derived from the manifest file name, while a separate friendly name parameter is used for display purposes. This redundancy ensures that even if the manifest contains errors, the correct package name can still be retrieved for error reporting. A more in-depth discussion on manifest structure will be provided in a separate article.
Language Module Implementation
Plugify’s API facilitates language module development through the ILanguageModule interface. This interface is exported by the language module library and used by the Plugify core for interaction. To maintain compatibility and prevent symbol discrepancies, language modules must be compiled with the same C++ version and compiler as the core. Differences in STL implementation and symbol mangling across compiler versions necessitate this requirement.
While trivial types and structures are commonly used, memory safety is enhanced by using consistent memory allocators. By default, standard allocators are employed, but it is recommended to maintain uniform build environments for plugins and language modules. Debug and release builds may use different allocators, potentially leading to crashes, but Plugify can detect and signal build mismatches.
To ensure backward compatibility, the API employs hybrid export handle classes, allowing language modules to remain compatible with newer Plugify core versions without requiring recompilation. This approach reduces reliance on versioned interfaces, simplifying updates and extending the longevity of core-module compatibility.
On certain platforms like Windows, the core library’s name is significant for function imports. In some implementations, Plugify is linked as an object library to accommodate this requirement. However, dynamic and static linking options are also available, configurable via the CMake build system.
Plugin and Language Module Loading
The Plugin Manager loads language modules in the order they appear within designated folders. However, since language modules do not depend on each other, their exact initialization sequence is inconsequential. Each language module is uniquely identified by a language name
parameter in its manifest file, and only one module per language can be loaded at a time. This design allows for easy module replacement while preventing conflicts caused by duplicate language modules.
Before loading plugins, the system sorts them based on dependencies using Depth-First Search (DFS). This ensures that plugins load only after their dependencies are available, enabling precise control over the execution order of lifecycle functions (onStart
, onUpdate
, onEnd
). However, cyclic dependencies can lead to issues, necessitating careful dependency management. Dependencies are declared in plugin manifest files, optionally specifying required versions. Once plugins are loaded, individual unloading is not supported to prevent dependency-related crashes. Instead, all plugins and language modules must be unloaded simultaneously.
d
Some language modules, such as those for .NET and Go, cannot be unloaded due to runtime constraints. Unloading these modules after initialization would trigger critical errors related to garbage collector.
Package Management
The Package Manager oversees local and remote package handling, scanning predefined folders for manifest files (termed "packages" in Plugify). It retrieves information about remote packages from repository links specified in plugify.pconfig
. Using this data, it can download, update, and manage packages as needed.
To maintain system stability, changes to package configurations require unloading the Plugin Manager, ensuring that all language modules and plugins are in an unloaded state before modifications are applied. Additionally, the Package Manager enables the creation of snapshots, allowing users to easily transfer plugins, modules, and configurations between machines or share builds with others.
For remote package management, Plugify utilizes WinHttp on Windows and CURL on other platforms. Packages are archived in ZIP format and extracted using the miniz library. The Package Manager employs the glaze library to handle JSONC manifests, which must strictly adhere to predefined schemas. If remote package functionality is unnecessary, it can be disabled during the core build.
Versioning and Updates
Plugify adheres to Semantic Versioning 2.0. Plugin and module developers should use major version increments for breaking API changes, minor versions for additions, and patch versions for non-breaking modifications. This practice ensures compatibility and smooth updates for users.
Communication Mechanism
The system employs functions based on the C calling convention, supporting both x64 and AArch64 architectures. This approach is chosen because most embedded languages expose C or C++ APIs, ensuring broad compatibility across various environments.
In some language modules, functions are dynamically generated at runtime to handle parameter marshaling between C and managed environments. The AsmJit library facilitates this process. Consequently, Plugify supports a limited but comprehensive set of parameter types, encompassing nearly all C data types and certain custom types such as strings, arrays, and variants. These are passed as C++ objects by reference, while supported C structures are also passed by reference to simplify compliance with calling conventions. Function return values are always passed by value to prevent memory leaks and maintain clear memory ownership in C++ interactions.
Functions and delegates between plugins are shared via C function pointers. Language modules handle allocation and ensure function validity until unloading. Each function must have a strict parameter description and order, with variadic parameters explicitly unsupported. However, alternative solutions exist for flexible parameter handling. Since plugins interact directly with each other to maximize performance, care must be taken when exposing methods across languages to prevent crashes and memory leaks. Some language modules could introduce minor overhead due to type conversions and runtime checks, while others, such as C++, impose no additional call cost, ensuring near-native execution speed. A more detailed discussion on marshalling and plugin interactions is covered in the language module development section.