Brass SSE Developer Guide - Tutorials
Tutorial 8 - Accessing the Win32API
If you don't know what the Win32API is and you've
never written a Windows program before then you can safely skip
this section. If you'd like to access the Win32API from your plugins
then read on!
We're going back to our tictactoe game for this
tutorial, so you can:
Download the Source
Code for this tutorial here!
What is the Win32API
The win32api is the way Microsoft exposes the internals
of Windows to programmers, allowing you to access everything from
message box functions to registry editing. If you've done even some
Visual Basic development you will have come across this, and if
you're a C coder then you'll definitely know all about it!
It is a collection of functions, stored ("exported")
from system DLLs in the Windows installation directory, and if you
want to do pretty much anything with Windows then you'll need to
access it.
Error Handling and the Win32API
Now that our game is working we should think about
handling any errors that might occur. As you might have noticed
when you first added the code, if the createimage
statement in the Init handler fails to load an image, nothing happens.
No error, no warning, just no image. It's probably time we addressed
that.
The createimage statement
returns a boolean status to tell you whether loading the image succeeded
or failed. That makes it really easy to create an error handler
if we can't load an image:
if(createimage(SymbolX,
"SSE\\TicTacImages\\tictac-x.gif") == False)
{
// Uhoh, image
loading failed, error handler here
}
There's a lot of things we could do in the error handler
code - we could abort the plugin, try to load a different image
or even set a flag to tell the Render handler to draw the X and
O manually.
How do you know when statements return
values and what they mean? Simple - you check the Shiny language
reference, linked on the front page of
the developer guide.
Much more fun and useful would be to pop up a messagebox
explaining the error to the user. Unfortunately there isn't a statement
in Shiny to pop up a messagebox, so how do we do it?
Simple - we use the win32api!
Actually this tutorial is lying. Shiny comes
with an extension library that has a Messagebox function with
more powerful features than the Win32API. But this is a good
example, so we'll go with it :)
Accessing the API
As mentioned before, from our point of view the win32api
is a bunch of functions exported from a bunch of system DLLs. What
we need to do is to somehow load those DLL's in our plugin and use
the functions we need. To do this we use the import
statement.
The import statement
tells the Shiny compiler that you want to declare a function exported
from a DLL as a function in your code. Once imported you can call
that API function just as you'd call any other function you wrote
yourself.
If you're a coder and you're wondering about
importing functions from your own DLLs, yes you can do it using
exactly the same process. Take note of the order folders are
scanned for matching DLLs below.
Most importantly you must declare your exported function as
STDCALL. If you don't, you'll get a Windows error about
the value of ESP not being saved across calls. This is to be
expected because the Brass VM uses STDCALL for all imported
functions. The reason for this is the Win32API is entirely STDCALL,
and the import system was designed primarily to work with the
Win32API.
The Shiny import system works almost exactly like
the Visual Basic import system. This
tutorial is a good introduction to how VB imports and uses the
win32api, it's a recommended read because apart from a couple of
small syntax differences the Shiny import system is the same. It
also explains how to find API functions and how to call them.
Here's the prototype of the import statement:
import dll_name return_type
function_name ( optional_parameters );
The easiest way to explain the import system is to
see an example. Let's import the timeGetTime function. According
to MSDN at the bottom of the page this function is included
in the winmm.lib library, which means it's in the winmm.dll DLL
(just replace ".lib" with ".dll" to find the
DLL name). The function prototype is:
DWORD timeGetTime(VOID);
And as the web page says, it takes no parameters and
returns the time in milliseconds. So how do we import this function?
First we make sure we have all the necessary information:
- What function name do we want?
- What DLL is it in?
- What parameters does it take?
- What type does it return?
Now it's a question of filling in the blanks in the import statement:
import winmm dword timeGetTime();
There are 2 things to notice about this import
declaration. Firstly the function name in the statement has exactly
the same capitalization as MSDN showed us. This is extremely
important. Imported function names are case sensitive - specifying
timegetTime would fail.
Secondly both the ".dll" extension and an explicit path
weren't included for the DLL. This is a sanity-check the Shiny compiler
forces you to perform. An imported DLL must of course be a .dll
file, so there is no reason to include the .DLL extension in the
import statement. This is exactly
the same as Visual Basic. An imported DLL must also be available
to everyone who uses the plugin. If the compiler allowed you to
specify a path it would be way too easy to use a path that doesn't
exist on any other computer. Instead when the Brass Virtual Machine
reads the import declaration and tries to load your imported function
it looks for the DLL in these places (and in this order):
- In the <Brass Install Folder>\SSE\<SSENAME> folder
- The current directory (the Brass installation folder)
- The system path (includes the Windows folder)
- In the <Brass Install Folder>\Extensions folder (this
folder may not exist, which is fine)
- In the <Brass Install Folder>\SSE folder
- In the <Brass Install Folder>\DLL folder (this folder
may not exist, which is fine)
Brass stops looking when it finds the first occurrence
of this DLL, and will display an error if it can't find the DLL
in any of these locations. You'll see why these folders are searched
when we look at Shiny extensions.
You must always place
your import statements at the TOP of your plugin code.
This is extremely important. If you call an imported function before
the Shiny compiler has found the import statement it will think
you're calling a function you've coded yourself. The import statement
is how the Shiny compiler knows you're linking to an external DLL
and that it won't find the code for this function call.
Sometimes you may need to import 2 functions
with the same name from 2 different DLLs. The import statement
supports this via the "as" keyword. This is documented
in detail in the import language
reference.
Okay - you've put the import statement at the top
of your code and you're ready to call the function. How? Easy, just
call it like a function!
dword CurrentTime = timeGetTime();
Importing Functions Without Return
Values
Sometimes you'll need to call an imported function
that doesn't return anything. To do this, declare the imported function
as returning void, like this:
import mydll void DoesntReturnAnything();
A More Complex Example
The timeGetTime() function is a useful example,
but it's not very realistic. Most API functions you call will need
to take some parameters. So how do you pass parameters to an API
function? Let's go back to our plugin code - we wanted to display
a messagebox error, remember?
The win32api function to display a messagebox is
called, not surprisingly, MessageBox. You can find it at
MSDN here. Careful though, because this function has a whole
bunch of things that will trip us up.
Open the MSDN page and scroll to the very bottom.
Uhoh, danger, the Cylons are coming, the Ori are here...

As you might be able to tell by the all the red
lines, we've found a classic Win32API trap. This is the bane of
Visual Basic programmers everywhere.
At the top of the page, MSDN says the function
is declared like this:
int MessageBox(
HWND
hWnd,
LPCTSTR
lpText,
LPCTSTR
lpCaption,
UINT
uType
);
But if you try and import the MessageBox function
Brass will display an error. Why? Because even though MSDN says
otherwise, the MessageBox function doesn't exist.
The clue is in the 2 circled statements in the
image of the webpage above. For a number of reasons Microsoft declares
2 types of a lot of the API functions. One type is "ANSI",
the other type is "UNICODE". These refer to the size of
the text strings passed to the function. By size we're not talking
about length, it refers to the underlying way the string data is
stored.
This all came about because not everyone in the
world speaks English (shock horror). Languages like Chinese require
extended characters to represent the entire alphabet, and all these
characters don't fit into good old ANSI strings. To solve this the
larger UNICODE system was created.
You don't have to care about any of that though.
What you do care about is the function name. When you see a function
that has both an ANSI and a UNICODE implementation (look at the
line of text circled at the bottom of the MSDN page) it means that
function doesn't actually exist. What does exist are 2 versions
of that function with exactly the same name, but suffixed with A
for ANSI or W for UNICODE (W stands for "wide"). So what
we really have is this:
MSDN says:
int MessageBox(
HWND
hWnd,
LPCTSTR
lpText,
LPCTSTR
lpCaption,
UINT
uType
);
What the function is really called:
int MessageBoxA(
HWND hWnd,
LPCTSTR lpText,
LPCTSTR lpCaption,
UINT uType
);
int MessageBoxW(
HWND hWnd,
LPCTSTR lpText,
LPCTSTR lpCaption,
UINT uType
);
Whenever you import a
function with Shiny, you must ALWAYS use the ANSI version
- MessageBoxA. Not all functions will
have ANSI and UNICODE versions, some wil be like timeGetTime and
only have one implementation. How do you know which functions have
what? You don't, and that's what gets Visual Basic programmers so
stressed.
Fortunately VB programmers have been stressed about
this for a while, and there are thousands of websites that list
how to import each of the Win32API functions into a VB app. Shiny
was deliberately created to be compatible with the VB way of importing
functions, so you can simply search for a VB solution to your problem
and use that.
Here's an example. Let's say we wanted to get the
current directory into a string in our plugin. To do that we use
the GetCurrentDirectory() win32api function. Here's the
MSDN page for it. At the bottom of the page we've got that famous
line:
"Implemented
as GetCurrentDirectoryW (Unicode) and GetCurrentDirectoryA (ANSI)"
Let's say you were having trouble understanding
how to import this function and it didn't clearly show you the ANSI
function name. If we go to Google and search for:
"visual basic"
how to import getcurrentdirectory
The third result is a link to a forum where someone
is asking "How can I call GetCurrentDirectory() from VB?".
If we look at the answer
they received, we see this:
private Declare
Function GetCurrentDirectory Lib "kernel32" Alias
"GetCurrentDirectoryA" (byval nBufferLength as Long,
byval lpBuffer as string) as Long
And hey presto some VB user has just told us to use "GetCurrentDirectoryA"
for this function. But what's all that "byval" junk at
the end?
Passing Parameters to API Calls
Before we got sidetracked into the wonderful world of Chinese we
were thinking about passing parameters to our MessageBoxA function:
int MessageBoxA(
HWND hWnd,
LPCTSTR lpText,
LPCTSTR lpCaption,
UINT uType
);
Ignore the first and last parameters for a second, let's concentrate
on the middle 2. According to the MSDN page the second parameter
is the text you want to display and the third is the caption (title)
of the message box. So how do we call this function with some sensible
text?
First we need to tell the Shiny compiler that our imported function
takes some parameters. We do this in the import declaration:
import user32 int MessageBoxA(int, string,
string, int);
Here we're saying to Shiny, "I want to import the MessageBoxA
function from the user32.dll, it returns an integer and accepts
an integer as its first parameter, a string as it's second and third,
and another integer as its fourth".
All we're doing is translating the MSDN function prototype into
Shiny language types that the compiler can understand. This is exactly
the same way it's done in Visual Basic.
Now when we call our function, we just supply parameters to it
as though it was a normal function we coded:
int Result = MessageBoxA(0, "I'm a message!",
"The Title", 0);
Let's see this in action by adding the code to our plugin.
On line 1 of your plugin code, add this
import statement:
import user32 int MessageBoxA(int, string,
string, int);
In the Init handler, change
the SymbolX createimage statement to this:
if(createimage(SymbolX, "SSE\\TicTacImages\\NOT-THERE.gif")
== FALSE)
{
MessageBoxA(0,
"Couldn't load the X image", "Error", 0);
}
Note the new filename, we deliberately want to
cause an error by loading a non-existant file. Test the new code.
Click the "Compile" icon (
) then the "Execute" icon (
)
If you typed everything correctly (or incorrectly
in the case of the filename) you'll get this error message when
your plugin loads:

Your first API import - nicely done!
The Tokens and Tables Window
In the last tutorial you saw some of the debugging
systems in SSEdit. If you open the Tokens and Tables window you'll
now see that your imported MessageBoxA function appears in the Import
Table.
How Does It Work?
There's a smart bit of logic inside the Brass Virtual
Machine that understands how to convert between Shiny types and
types that the Win32API can understand. Because it's all handled
by the Virtual Machine you can still benefit from implicit type
conversion. For example, this is perfectly legal:
int MyVal = 99;
MessageBoxA(0, MyVal, "Error", 0);
Even though the second parameter was imported as a
string, you can supply an integer to it. The Shiny compiler looks
at the import declaration and sees
that it needs to pass a string to the API function, but that you
supplied an integer. It then automatically performs a type conversion
to convert the integer to a string to make it compatible with the
API call.
ByVal and ByRef
If you've done any VB programming you'll know all
about ByVal and ByRef. We're not going to cover them in detail here
because again, the Shiny byval and byref work exactly the same way
as the Visual Basic ones. MSDN explains
them in detail.
The byval and byref
keywords are only valid for import statements. You cannot use
them with normal functions.
If you need to call a Win32API function that modifies
the parameter being passed to it, specify byref
in the import declaration:
import mydll int ModifyVars(string byref);
You call the function in the same way:
string MyString = "This will be changed";
int Result = ModifyVars(MyString);
When the function returns, MyString will be updated
with whatever modification the function made to it.
The byval keyword is
used by default so you don't need to specify it (you can if you
like):
import mydll int MyFunction(string byval);
is the same as:
import mydll int MyFunction(string);
Remember this simple difference:
- The byval keyword allows variables
and constants to be passed as arguments. The contents of a variable
cannot modified
- The byref keyword only allows
variables to be passed as arguments. The contents of a variable
can be modified.
Preallocating String Space
Lots of Win32API functions require a string to have
space pre-allocated inside it for whatever it's going to receive.
There are long and complex technical reasons for this which you
really don't have to worry about. Let's look at an example, GetCurrentDirectory.
The GetCurrentDirectory documentation states this:
DWORD GetCurrentDirectory(
DWORD
nBufferLength,
LPTSTR
lpBuffer
);
Parameters
nBufferLength
[in]
The length of the buffer for the current directory string, in
TCHARs.
The buffer length must include room for a terminating null character.
lpBuffer
[out]
Pointer to the buffer that receives the current directory string.
This
null-terminated string specifies the absolute path to the current
directory.
To determine the required buffer size, set this parameter
to
NULL and the nBufferLength parameter to 0.
VB programmers will be very aware of the catch here.
The lpBuffer parameter is a string, and this string receives the
current directory. nBufferLength specifies how much space is available
in that string to store the directory string. No problem there,
you might think.
Unfortunately this is a disaster waiting to happen.
In VB, if you pass a string that only has 100 characters allocated
to it as the lpBuffer parameter, but pass a number higher than 100
to the nBufferLength parameter, the application will crash. If you're
interested why then read the next boxout.
The cause of this problem is the dreaded "buffer
overflow" error. In a higher level language like Shiny
and VB, string appear to be automatic - you can size them, exchange
values and use them however you want. In C though, strings are
core array datatypes. Just like if you specify an invalid index
when accessing a Shiny array, if you don't provide a buffer
of the size the API function is expecting many bad things will
happen.
This problem isn't actually VB's fault, it's a programmer
error. The programmer is responsible for making sure the string
has enough space in it to store the data it will receive, and that
the length of the string passed to the function in nBufferLength
is accurate.
This is true for Shiny as well. If you call an API
function that requires a string to be of a specific length and you
do not provide a string of that length, your plugin will crash.
So how do you allocate strings of a specific length?
Enter the stralloc statement.
This statement takes a string and an integer and preallocates the
required space in the string. It's absolutely essential for a lot
of API calls.
stralloc(string variable,
space to allocate);
Here's an example:
string MyString;
MyString = "12345";
// MyString is 5 characters long
print strlen(MyString);
// Allocate 100 characters to MyString
stralloc(MyString, 100);
// MyString is 100 characters long
print strlen(MyString);
A Quick Recap
Now you know pretty much everything you need to write
any plugin you want! Using the win32api gives you access to all
of Windows, so there's nothing you can't do now.
In this tutorial you saw:
- How to import Win32API functions
- How to tell if a function is ANSI
or UNICODE
- How to use byref and byval
- How to preallocate string space
The next tutorial is going to cover bitwise operations
and a few other useful things before we wrap up the series. Before
moving on you should play around with function importing and get
comfortable with how it works.
|