Building a basic plugin system in BlitzMax (Win32)
BlitzMax comes with functionality for loading external libraries - these are “.dll” files on Windows, “.so” files on GNU/Linux, and “.dylib” files on MacOS. This functionality can be used to load code from outside of the running application, which is useful for building plugin systems.
The method to load libraries is slightly different depending on the platform. This article will concentrate on the Windows way of doing things as it’s the most straightforward. In a future post I’ll go over methods that work on MacOS & Linux.
- What we’re building
- Loading a DLL file in BlitzMax
- Wrapping things up in a Type
- Scanning a directory and loading plugins
- Running plugin functions
- Creating a DLL in BlitzMax
- Putting it all together
What we’re building
For this exercise we’re going to build a small BlitzMax application that does the following:
- Scans a directory for DLL files (plugins).
- Loads each plugin file.
- Runs a function in each plugin.
- Displays the result.
At the end we’ll also create a DLL written in BlitzMax and use our new application to load it.
The complete source, as well as the example DLL files, is available at the end of this post.
But before we get started, let’s look at the key functions we’ll be using.
Loading a DLL file in BlitzMax
The pub.win32
module contains functions for opening DLL files. We’ll be using the following:
- LoadLibraryA :: Loads the DLL file into memory.
- GetProcAddress :: Gets the address of a function within the loaded DLL.
LoadLibraryA
returns an integer handle to the loaded library (or false
if the library could not be loaded).
GetProcAddress
returns a Byte Ptr
which BlitzMax can use as a function pointer. This makes it possible to call a function that was defined in the DLL, as well as getting any returned data.
In practice it looks a little bit like this:
Import pub.win32
' Load the library.
Local library:Int = LoadLibraryA("library.dll")
' Get a reference to the function we want to call.
Local doSomething:String() = GetProcAddress(library, "do_something")
' Call the function.
Print doSomething()
' => This will call `do_something()` in the DLL.
(The above example assumes “library.dll” has an exported function called do_something
.)
Wrapping things up in a Type
Now that we have the basic idea laid out, let’s wrap things up in a Type
. This will make it easier to manage multiple DLL files later on.
We’ll create a simple type called Plugin
that contains a process
field. This field will be a function pointer that accepts and returns a string:
Type Plugin
Field path:String
Field library:Int
Field process:String(in:String)
' Get just file name of the plugin.
Method pluginName:String()
Return StripDir(StripExt(Self.path))
End Method
Function Load:Plugin(path:String)
Local this:Plugin = New Plugin
this.path = path
this.library = LoadLibraryA(path)
this.process = GetProcAddress(this.library, "process_string")
Return this
End Function
End Type
We can now load a plugin and call a function within it with just a few lines:
Local p:Plugin = Plugin.Load("library.dll")
p.process("my string")
This will be enough for our example, but in the future we’d want to be more defensive: there’s no check that the file exists, that it’s a valid DLL file, or that the function pointer is valid.
Other improvements would be allowing the library function name (“process_string”) to be customized, and we’d probably want to load multiple functions too.
For now we’ll move on to scanning a directory for plugin files.
Scanning a directory and loading plugins
There are a couple of ways to do this. We can use ReadDir and NextFile
to open a directory and read the contents. Alternatively we can use LoadDir to load the contents into a string array.
For this example we’ll use LoadDir
as it’s pretty straightforward to implement and we’re assuming that our plugin directory will just contain DLL files.
LoadDir
does not recurse into sub-directories, so for this example we’ll assume there is only a top-level “plugins” directory. We’ll scan a directory for DLL files, load them into a Plugin
instance, and store them in an array:
Import brl.filesystem
Local plugins:Plugin[]
Local pluginFiles:String[] = LoadDir("plugins")
For Local fileName:String = EachIn pluginFiles
' Load the plugin file.
Local p:Plugin = Plugin.Load("plugins/" + fileName)
' Expand the plugins array and add our instance to the end.
plugins = plugins[..plugins.Length + 1]
plugins[plugins.length - 1] = p
Next
Now that we have our array of loaded plugins, let’s use them to do something.
Running plugin functions
To keep things simple our plugin files will contain a single function called process_string
. This function will accept a string, manipulate it in some way, and then return the result (we’ll also build a plugin using BlitzMax to test this).
' Create our string and print it.
Local ourString:String = "Hello, World!"
Print "[initial] " + ourString
' Run each plugin on the string.
For local p:Plugin = EachIn plugins
ourString = p.process(ourString)
Print "[" + p.pluginName() + "] " + ourString
Next
This will give us an output that looks something like this:
[initial] Hello, World!
[downcase] hello, world!
[reverse] !dlorw ,olleh
Pretty neat! You’re not just limited to using basic data types either; you can use any BlitzMax object, including custom types.
Creating a DLL in BlitzMax
The original BlitzMax supports building applications and modules, but not libraries. Thankfully bmx-ng added a makelib
command which can create libraries from BlitzMax code.
We’ll create a simple DLL to convert a string to lower-case:
' downcase.bmx
SuperStrict
' Don't want the DLL to import everything.
Framework brl.basic
Function process_string:String(in:String)
Return in.ToLower()
End Function
And compile it using bmk
:
$ bmk makelib -r downcase.bmx
DLL files compiled with BlitzMax also need a def
file that contains their exported functions. bmk makelib
will generate this for you, but if you want to manually create it you can. For our example it would look like this:
EXPORTS
process_string=bb_process_string
BlitzMax adds a bb_
prefix to compiled functions, so remember to add that when exporting.
More information is available here: Creating DLLs.
Putting it all together
We now have our application, along with a DLL file that can be loaded as a plugin. The finished application looks like this:
SuperStrict
Framework brl.basic
Import pub.win32
Import brl.filesystem
' Load all plugins.
Local plugins:Plugin[]
Local pluginFiles:String[] = LoadDir("plugins")
For Local fileName:String = EachIn pluginFiles
' Load the plugin file.
Local p:Plugin = Plugin.Load("plugins/" + fileName)
' Expand the plugins array and add our instance to the end.
plugins = plugins[..plugins.Length + 1]
plugins[plugins.length - 1] = p
Next
' Create our string and print it.
Local ourString:String = "Hello, World!"
Print "[initial] " + ourString
' Run each plugin on the string.
For local p:Plugin = EachIn plugins
ourString = p.process(ourString)
Print "[" + p.pluginName() + "] " + ourString
Next
Type Plugin
Field path:String
Field library:Int
Field process:String(in:String)
' Get just file name of the plugin.
Method pluginName:String()
Return StripDir(StripExt(Self.path))
End Method
Function Load:Plugin(path:String)
Local this:Plugin = New Plugin
this.path = path
this.library = LoadLibraryA(path)
this.process = GetProcAddress(this.library, "process_string")
Return this
End Function
End Type
The full source can be viewed on github: sodaware/blitzmax-plugin-example or downloaded directly.