» Home

  » Developer Guide


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):

  1. In the <Brass Install Folder>\SSE\<SSENAME> folder
  2. The current directory (the Brass installation folder)
  3. The system path (includes the Windows folder)
  4. In the <Brass Install Folder>\Extensions folder (this folder may not exist, which is fine)
  5. In the <Brass Install Folder>\SSE folder
  6. 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.