Extending CoolBasic with your own DLLs
One of the shortcomings of the old CoolBasic is its limited extensibility. The original design didn’t really support utilizing external code. If you wanted to create a “library” it’d have to be written in CoolBasic and then you’d have to give away the source code. The end user would then include those .cb files in the beginning of their programs.
As CoolBasic is interpreted, the need to execute code with native speed soon become apparent. To relief this limitation, CallDll
was introduced. You’d allocate memory for the in parameters and the return value. It’s not a very resourceful way to support or use external DLLs, and I would have liked to implement a proper syntax like Declare Function GetTickCount Lib "kernel32" Alias "GetTickCount" () As Integer
. I never did, though.
Something like that may still come in for CoolBasic Classic. It’s on the TODO list, and both the compiler and runtime have a preliminary support for it already in place. But before diving deeper into that, there’s an alternative way to integrate external libraries so that you can use them directly in code. You’d call them just like any other user-defined function, and you don’t have to declare them before use.
Functions owned by the Host
The CoolBasic Classic language itself doesn’t include functions such as LoadImage
or PlaySound
. They’re provided by the engine that’s interpreting the compiled program. The design philosophy here is that CoolBasic Classic is just a language, and there can be any number of game engines that can run CoolBasic code. One engine may offer a completely different set of commands than the other (and this is also where project types come into play, more about that later.)
The engine basically gives a list of commands available in it. This list of commands is then fed to both the code editor (so it can syntax highlight,) and the CoolBasic Classic compiler (so it recognizes them.) Within the executing engine, CoolBasic functions are called in a certain way, and functions provided by the hosting engine are called differently (since they’re native code.)
A host function defines an ID that is presented to the CBC compiler. Let’s say a function “PlaySound” has an ID of 15. When the compiler emits the CallHost
instruction it also attaches the number 15 on it. At runtime, the virtual machine will call a delegate who had defined an ID of 15.
Nice, so that’s how we call native code from a CoolBasic program. But there’s a little design flaw here… What if we wanted to import functions from another DLL and they had ambiguous IDs.
Introducing function signatures
A better way to refer to a function is by signature. The signature comprises of the function’s name, and the number and types of its parameters (but not its return type.) Based on the supplied arguments, the compiler determines exactly which function to call (and ambiguous function candidates would generate a compile time error.) Therefore, it’s sufficient to just issue “Call a method with this signature” at runtime because the compiler has already enforced that there will be no ambiguity.
So I spent some time refactoring the CoolBasic Classic compiler and the Cool VES virtual machine to now operate with signatures instead of arbitrary function IDs. When the engine is loading, it inspects the available host functions and gathers their signatures. The signatures along with the execute delegates are stored in a dictionary. If there are multiple functions with the same signature, it throws an error. This ensures that the call will be explicitly directed to the same function that the compiler determined. When the interpreter is calling a host function, we’re going to peek into the signature dictionary and execute the appropriate delegate.
This approach makes it possible to have multiple sources of host functions (as long as the provided functions’ signatures don’t collide.) The primary source is the game engine itself. But we can also dynamically inspect the other DLLs, within the same directory, for any valid host functions and import those! Think of it like this: the linking process is done at runtime (rather than after compilation) when the CoolBasic program is loaded into the engine’s memory.
All you have to do is drop those DLLs in the same directory as the executing engine. They’ll run at native speed when executed by the CoolBasic program.
Implementing host functions
It’s reasonably easy to implement a host function that is callable from a CoolBasic program. All you need to do is create a managed DLL in any CLR compliant language, such as C# or VB.NET, that contains methods that satisfy this delegate:
[csharp]delegate void CommandAction(StackEntry[] stack, ref int stackIndex);[/csharp]
Let’s create a DLL that introduces a Sleep
command that you can then use in your CoolBasic programs. It takes one integer as parameter, the number of milliseconds that the program will wait until continuing execution.
I’ll create a new Class Library project in Visual Studio, name it “MyCoolExtension” and then create a single class “MyCommands” in it. I then add a reference to CoolVES.dll and write this code:
[csharp]namespace MyCoolExtension
{
using System.Threading;
using CoolVES;
using CoolVES.Integration;
public class MyCommands
{
/// <summary>
/// Halts the program until the specified amount of milliseconds have elapsed.
/// </summary>
/// <param name="stack">The stack.</param>
/// <param name="stackIndex">Index of the stack.</param>
/// <remarks>Sub Sleep(time As Integer)</remarks>
[Command(Id = 1, Name = "Sleep", ReturnType = DataType.Void)]
[CommandParameter(Index = 0, Name = "time", DataType = DataType.Int32)]
public static void Sleep(StackEntry[] stack, ref int stackIndex)
{
var time = stack[stackIndex–].AsInteger;
Thread.Sleep(time);
}
}
}[/csharp]
Build this and drop it to the same directory as the final game.
Note that you’ll have to handle stack manipulation manually. This is a speed optimization. It’s nothing too complicated though; you’ll simply access the stack array and decrease the pointer for as many times as you have parameters in your function. If you specify something else than Void
as the return value type, you’ll also have to assign a value onto the stack at the end, too.
There’s one additional step that needs to be done in order to have the compiler support this new command. There will be a tool with which you can generate a “definition file” out of this DLL. The file contains metadata that describes the functions and constants contained within your library. You only have to do this once. This file is provided to the CoolBasic Classic compiler in the command line.
Definition file handling will be done automatically behind the scenes by the code editor. Also note that you only need the file in development time; metadata doesn’t (and shouldn’t) be present in the final game output directory.
As a library developer, when you want to distribute your work, simply provide the compiled DLL along with the generated metadata file. To use the library, the end user would make a reference to the metadata file in their code editor. This would make all new functions and constants available for syntax highlighting and compilation.
If one can programmatically generate the metadata out of a compiled DLL, why would we need a separate metadata file? Can’t we just inspect the referenced additional DLLs for this information on-the-fly when compiling? Well, no… that would create a strong dependency between the CoolBasic Classic compiler and CoolVES virtual machine. The idea is to decouple CoolBasic Classic as a language from any game engines (or execution engines, rather.) The metadata format has been designed to be quite flexible and can describe much more complicated type information than what CoolVES provides currently. In other words, the metadata can span across current and future coming technologies.
All in all, I think this design addresses the requirements in a neat way:
- You can achieve machine code speed
- It’s easy to create these DLLs
- You can harness the full power of the .NET and Mono frameworks
- It’s easy to consume these DLLs. They integrate to editor, compiler and runtime automatically. No need to declare the functions in code before use
It doesn’t allow you to call unmanaged DLLs without creating a managed wrapper, though. Perhaps that’s better handled with a Declare Function
statement…