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

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 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.