MultiPlug: Usage manual
Importing a plugin module
To import a plugin module, the function multiplug.importer(filename)
is
used. The function automatically determines the plugin implementation
language from the file extension (.rs
for Rust, .sh
for Bash, .nim
for Nim, .py
for Python).
To load a plugin with a specific language only, use one of the functions
bash(filename)
, rust(filename)
, py(filename)
or nim(filename)
.
Additionally to the filename the following named parameters are available:
verbose
: output verbose messagesdisable_bash
(importer()
function): if set, only Rust, Python and Nim plugins are supportedreq_const
,req_func
: list of names of constants and functions, which must be provided by the module; an exception is raised otherwiseopt_const
,opt_func
: list of names of constants and functions, which may be provided by the module and are set toNone
if not providednim_const_pfx
: prefix to use in the Nim constants export system (default:py_const_
)rust_const_cls
: name of the class to use in the Rust constants export system (defailt:Constants
)
Bash plugins limitations
For plugins written in Bash there are some limitations:
the return type of the functions, and the constants type is limited to:
strings
lists of strings
lists of lists of string
the function arguments are not checked, i.e. the wrapped function interface is always
(*args, **kwargs)
the script code is sourced multiple times in order to collect information about the defined functions and constants
If this limitations are undesired in an application, bash plugins can be
disabled by setting disable_bash
.
Plugin Functions
In Python plugins, functions are just defined as in any Python module.
In Nim plugins, the {.exportpy.}
pragma from the nimpy
Nim library
is used. Consult the nimpy
documentation for more information.
In Rust plugins, the PyO3
library is used. Consult the library
documentation for more information.
In Bash plugins, functions are defined using some conventions, described
below, which allow to automatically create their Python wrappers.
Rust plugin dependencies
Since the Cargo.toml
for the plugin is autogenerated, dependencies other
than pyo3 must be listed in the plugin code file, using the syntax:
// [dependencies] ..."
where ...
is what shall be writte in the dependencies
section of
Cargo.toml
.
Bash functions arguments
All exported functions defined in Bash plugins are wrapped in a function
which accepts any number of arguments and keyword arguments
((**args, **kwargs)
).
The Bash function, however, may only accept a defined number of positional arguments. An eval-based system is used for passing the keyword arguments, e.g.:
function foo() {
arg1=$1; shift; arg2=$1; shift # shift after each positional argument
# the remaining arguments are the keyword arguments
kwargs=$*; for kw in $kwargs; do eval $kw; done
...
}
Bash function return value
To return a value from Bash, a string is printed using “echo”. For that reason function shall not print anything else than the return value (however, it may use the standard error freely).
If the returned string contains multiple lines or tabs, the wrapper splits it into an list of strings. If both newlines and tabs are returned, the string is splitted first by newline, then by tabs, and a list of lists of strings is returned by the wrapper. Examples:
echo "123" # ==> "123" is returned (string)
echo "1/t2" # ==> ["1", "2"] is returned (list)
echo "1/n2/t3" # => [["1"], ["2", "3"]] is returned (nested list)
Constants
Actually, Python does not really have constants. However, by convention,
variables names written in UPPER_CASE
are considered constants.
Module-level constants are useful e.g. for defining special values, for defining hard-coded module parameters, or for storing metadata (for example, a plugin version number, or a description of possible plugin parameters).
In Python plugins, the constants are simply defined in the module code. For the other languages, some conventions are described below.
Bash
Constants are defined as variables in the script code. Only variables whose name does not start with an underscore are imported.
Scalar variables are imported as string constants. Arrays are imported as lists of strings. If any element of the array contain a tab, each of the elements is splitted by tab, so that the constant value is a list of lists.
Examples:
STRING_FOO1="bar1"
LIST_FOO2=( "bar21" "bar2 2" )
NESTED_FOO3=( "bar311"
"bar3 21\tbar322" )
Nim
Since the nimpy/nimporter
system does not allow to export Nim constants
directly to Python, a workaround is used instead. The constants are defined
as the return value of functions (which are exportable to Python).
The easiest way to import constants is to install the nimble package
multiplug_nim
(distributed with the source code of Multiplug) and
use the macro exportpy_consts()
, e.g.:
import multiplug_nim/exportpy_consts
const
FOO="foo"
BAR="bar"
exportpy_consts(FOO, BAR)
Implementation details
For each constant, a proc with no arguments is defined in the Nim code,
which returns the value of the constant. The proc name has a prefix
(by default py_const_
which allows to recognize it and is stripped
in the definition of the Python module attribute.
E.g.: proc py_const_FOO(): string {.exportpy.} = "bar"
defines the attribute FOO
with value "bar"
in Python.
Using a custom prefix
The prefix used in the proc names (by default py_const_
) can be changed by
setting the nim_const_pfx
parameter of the importer function. In this case
in the Nim code, the macro exportpy_consts_wpfx(pfx, ...)
is used in place
of the exportpy_consts()
macro, where the first parameter is the same
prefix used in the nim_const_pfx
parameter in the importer function in
the Python code.
Rust
Since the PyO3/maturin
system does not allow to export Rust module-level
constants to Python, a workaround is used instead. The constants are defined
in a struct called Constants
and exported as Python class
For example:
#[pyclass] struct Constants {}
#[pymethods]
impl Constants { #[classattr] const FOO: &'static str = "bar"; }
...
#[pymodule]
fn my_module(_py: Python, m: &PyModule) -> PyResult<()> {
m.add_class::<Constants>()?;
...
Using a custom class name
Instead of Constants
a different class name can be specified,
by setting the rust_const_cls
keyword parameter in the importer function
to a different string.
Persistant state between function calls
To implement a state which shall persist between function calls, the plugin shall implement an initialization function, creating the state, a finalization function, destroying the state (if necessary). The other function calls shall then take the state as an argument.
Examples of plugins using a persistant state in all supported programming
languages are in the tests/testplugins
directory.
For Python and Nim it is not particularly challenging to have an initialization function, which creates a new state object, and pass the state as an argument to other functions.
Rust
In Rust the state can be implemented as a struct
, e.g. struct State
.
The initialization function will return a PyResult<Py<State>>
. Thereby the
state is created using Py::new(py, State {...})
, where py
is obtained
using Python::acquire_gil().python()
.
The state is passed then to other functions (including the finalization
function, if any) as the state: Py<State>
argument. To change the state
let mut state = state.borrow_mut(py)
can be used, where py
is obtained
as explained above.
Bash
In Bash the state can be implemented by storing it to file. The initialization function can create a state file using $(mktemp) and storing some information to the file, then returning the filename. Other functions then get the state filename as an argument.
For example the information can be
stored in form of variable assignments (e.g. x=1
). Functions other than
the initialization get then the state filename as argument. The contents
of the file can be executed using eval $(cat $state_file)
.
If the variable are modified, the previous state file can be overwritten
with new variable assignment statements (e.g. x=2
).