Calling Native DLLs from Javascript

PinballY has a special set of Javascript extensions for calling external native code. These extensions are available through a system object called dllImport. (Oof, that's full of ambiguous looking letters, isn't it? That's D-L-L-Import, DLL standing for Dynamic Link Library.)

DLL Importing can be used to make calls directly to most Windows API functions from Javascript, to access system functionality beyond what's provided through the Javascript classes provided in PinballY. Rather than trying to provide built-in Javascript support for the massive number of Windows API functions, we thought it would work better in the long run to let you call Windows yourself, directly.

You can also use DLL importing to call your own custom code, written in any other programming language that can create DLLs, such as C, C++, Visual Basic, or C#. The PinballY DLL import system tries to make it relatively painless to call external code, by providing automatic type conversions between Javascript and native types, so that you can call external functions directly without having to worry too much about type conversions.

The DLL import feature is a PinballY add-on. You won't find it in Web browsers or in standard Javascript. (Which also means you won't be able to read about it in other Javascript manuals; you'll have to make do with the material here.)

The obvious warnings about calling out to native code apply: simple errors in native code calls can crash the whole program, and if you get creative enough, you can even destabilize the system, corrupt disk files, or do other system damage. Javascript and PinballY can't do much to protect against errant calls through the DLL layer, because the whole point is to give you access to the broader Windows native environment, which is all beyond the reach of Javascript's supervision.

An example

The DLL importer combines quite a lot of concepts and technologies, so there's a lot to cover. Before we get into the details, it might help to look at an example, to see how all of the pieces fit together.

This example uses a few basic Windows API calls to get a list of the current top-level windows on the desktop. One important thing to notice is that we're calling Windows functions directly from Javascript, without having to write any DLL code of our own. This is a complete example that you can drop into your main.js file and run, without having to set anything up outside of PinballY. (You can also use the DLL importer to call your own DLL code, but the point here is that you don't have to write a DLL just to call Windows functions. The DLL importer lets you call just about any native code directly.)

If you're already familiar with the basic principles of using DLLs in other languages - C, C++, C#, Visual Basic - you'll find that the basic process of calling a DLL is the same. The only differences come from the need to reconcile Javascript's very abstract dataype system with the low-level types used in native code. The DLL importer tries to smooth over those differences and make the exchange of data relatively automatic and painless, but there are some details that unavoidably poke through the covers.

// Define the native struct types used in the DLL functions we // want to call. These definitions go into an internal table in // the dllImport object, so we can refer to the types by name in // subsequent bind() calls once they're defined. We use "C" // language syntax to define native types, since that's what // all of the Windows APIs use. That way, we can usually just // copy and paste definitions from the SDKs. There's no need // to puzzle out how the types map to Javascript; we just state // everything directly in terms of the original, native types. dllImport.define(` typedef struct _RECT { LONG left; LONG top; LONG right; LONG bottom; } RECT, *PRECT, *LPRECT; typedef BOOL (CALLBACK *WNDENUMPROC)(HWND hwnd, LPARAM lParam); `); // Bind the DLL functions we want to call. This step creates a // callable Javascript function for each DLL function we need. // This is a one-time step; we can call these functions repeatedly // once they're bound. // // The return value from bind() is an object, with properties // named for the functions declared. E.g., User32.GetDesktopWindow() // will be a Javascript function we can call to invoke the native // Windows routine GetDesktopWindow(). let User32 = dllImport.bind("User32.dll", ` HWND WINAPI GetDesktopWindow(); BOOL WINAPI EnumChildWindows(HWND hwnd, WNDENUMPROC lpEnumFunc, LPARAM lParam); BOOL WINAPI GetParent(HWND hwnd); BOOL WINAPI GetWindowTextW(HWND hwnd, LPWSTR buf, int nMaxCount); BOOL WINAPI GetWindowRect(HWND hWnd, LPRECT lpRect); `); // Create a string buffer for retrieving window titles. Unicode // characters are equivalent to 16-bit integers, so we need a Uint16 // typed array to receive the string data. Uint16Array is a // standard built-in Javascript type that we can use anywhere a // Unicode string buffer is needed in a DLL call. // // One of the things that makes "C" style functions (like Win API // call!) harder to use than Javascript is that most strings have // to be set up as fixed-length character buffers. We have to // pre-allocate this array with the maximum string length we're // willing to accept. let title = new Uint16Array(256); // Create a native RECT struct for retrieving window coordinates. // The DLL importer lets us create native structs of any type that // we've defined via dllImport.define(). let rc = dllImport.create("RECT"); // Make a list of the names and positions of the direct children // of the main desktop window. EnumChildWindows() is one of those // Windows functions that takes a "callback" function defined by // the caller. Windows calls this function repeatedly, once for // each window. We can provide a Javascript anonymous function // as the callback. let wins = []; let parent = User32.GetDesktopWindow(); User32.EnumChildWindows(parent, (hwnd) => { if (User32.GetParent(hwnd) == parent) { User32.GetWindowTextW(hwnd, title, title.length); User32.GetWindowRect(hwnd, rc); wins.push({ title: title.toString(), rc: { left: rc.left, top: rc.top, right: rc.right, bottom: rc.bottom } }); } });

Binding a function

To call an external DLL function, you first have to "bind" the DLL function. This step connects the native code to Javascript. You tell the system the name of the DLL you want to call, the name of the function in the DLL, and the "type signature" of the function, meaning the datatypes of the arguments and return value. The bind operation uses that information to load the DLL into memory (if it's not already loaded), look up the native code address of the function, and create a Javascript function that represents the native code function.

let User32 = dllImport.bind("User32.dll", "HWND WINAPI GetDesktopWindow()");

Important: Be sure to use WINAPI keyword when required. Almost all Windows API functions use it. This specifies the "calling convention", which determines how arguments are passed on the native stack when calling the native function. Using the wrong calling convention will corrupt memory, which can crash the whole program or cause erratic behavior.

The return value from bind() is an object, containing one property for each function you declared, with the same name as the function. For example, in the call above, we get an object with the one property GetDesktopWindow.

Each property of the bind() object is a callable Javascript function that invokes the corresponding native DLL function. So when we want to call the native GetDesktopWindow() function, we can now just write:

let hwnd = User32.GetDesktopWindow();

The first argument to bind() is the name of the DLL, in this case the standard Windows library User32.dll, which contains many of the basic UI functions in Windows. The second argument is the declaration of the function's return type and argument types using "C" programming language syntax.

The DLL importer uses C syntax for the function declarations primarily because that's what's the Microsoft API documentation uses almost everywhere. This makes it easy to copy and paste API definitions from the SDK documentation directly into your Javascript code, often with no changes. The C syntax is used in all cases, even if you're using a DLL written in some language other than C. You can't substitute VB or C# syntax; you have to translate it to C terms.

You can bind several functions in the same DLL at once. You can do this in two ways:

The Javascript "back-quote" notation is especially handy for the long string option, since it lets you spread out a string across multiple lines. That makes a long list of declarations easily readable.

User32 = dllImport.bind("User32.dll", ` HWND WINAPI GetDesktopWindow(); BOOL WINAPI GetWindowRect(HWND, LPRECT); `);

DLL filenames and paths

When you bind a function, the DLL name you specify can be a plain filename, such as the "User32.dll" we've been using in our examples, or it can use a full, absolute path. In the absence of a path, Windows will look in the PinballY program folder for the DLL and then in the standard Windows system folders. That makes it a no-brainer to always use plain filenames (with no path) for all standard Windows DLLs. Custom DLLs of your own are also best loaded with no path name, as a path name will create headaches if you ever move any files around. It's best to copy your custom DLLs into the PinballY program folder to ensure that they can be found without having to specify any paths.

Windows SDK "A" and "W" functions

Many of the common Windows API functions have slightly different names in the DLL than in the SDK documentation. For example, there's no actual DLL function called MessageBox. Instead, there are two variants of the functions, one called MessageBoxA and one called MessageBoxW. The "A" stands for ANSI (8-bit character strings), and the "W" is for WIDE (16-bit Unicode strings).

Most Windows API functions that take character string arguments (directly or within structs) have these "A" and "W" variants. If you run into any strange missing function errors, double-check the SDK documentation to see if the function you're trying to call needs an "A" or "W" suffix.

In PinballY Javascript, it's best to use the "W" variants when you're faced with a choice. Javascript itself uses Unicode for all strings internally, so you'll avoid unnecessary string conversions by using the "W" functions.

Calling a bound function

Once a DLL function is bound, you invoke the DLL function simply by calling the Javascript function returned from the "bind" call.

let User32 = dllImport.bind("User32.dll", "HWND WINAPI GetDesktopWindow()"); let hwndDesktop = User32.GetDesktopWindow();

When you call a bound function, the argument type list that you provided in the "bind" call is used to automatically convert arguments from Javascript format to the native datatypes, and to convert the return value from native format back to Javascript. That means you don't have to worry too much about the type conversion details when you call a bound DLL function; you can mostly just treat it like any other Javascript function. See below for details on the how automatic type conversions are handled.

"Out" pointer parameters

Many Windows API functions return information in "out" parameters. That means that you pass a pointer to a variable to the function, and the function writes some result information into that variable before returning.

Javascript doesn't have any concept of "pointer to a variable". The closest equivalent in Javascript is that you can pass an object to a function, and the function can modify the properties of that object before returning. But that obviously won't work with native code, which has no idea what a Javascript object looks like.

To deal with this mismatch, the DLL importer lets you create native data objects, which you can then pass to DLL functions in the arguments.

For example, suppose you wanted to call a function that returns information in an "int" (integer) variable:

let MyDll = dllImport.bind("MyDll.dll", "void GetInfo(int *result)");

In C or C++, it's easy to call a function like this: you just pass the address of an "int" variable with the "&" operator. But of course that doesn't exist Javascript, nor does the entire concept of the "address" of something. So how do we call this function? The answer is that we have to create a native "int" variable, not a Javascript variable. We do that through dllImport.create(), specifying the type of the variable we want to create:

let i = dllImport.create("int");

Things get pretty trippy at this point; I hope you liked Inception. i is a Javascript variable that contains a Javascript object representing a native int variable. If you look at i itself, you'll see a Javascript object. If you want to get to the native int variable, you can use i.value, which is a getter/setter property that accesses the underlying native int variable. If you assign to i.value, you'll store a value in the native variable. If you use i.value in an expression, you'll get a Javascript number corresponding to the native int value.

If you pass i to a DLL function that requires a pointer to a native int, the DLL importer will pass the address of the native int variable. So this is the answer to the earlier question of how we call that GetInfo() function that requires a pointer to an int.

// call DLL function "void GetInfo(int *result)", passing // a pointer to the native int stored in NativeObject i MyDll.GetInfo(i); // on return, i.value reflects what GetInfo wrote into *result alert("The result from GetInfo is " + i.value);

For more on using these special native value containers, see NativeObject objects.

This "pass by pointer" method works for struct types, not just basic types like "int". That's important, because many Windows APIs use the same pattern with structs. So next up, let's look at how to define native struct types in Javascript.

HANDLE and HWND variables as "out" parameters

Some Windows APIs return handle values as "out" parameters rather than as the function return value. In such a situation, the API will take a HANDLE* or HWND* pointer as an argument, with the intention that the caller will pass in the address of a HANDLE variable, where the callee will store the requested handle value before returning.

You can code these calls same way as other "out" parameter calls:

For example, the OpenThreadToken() API returns a thread token as a HANDLE value in its fourth argument, which is specified in the API as a HANDLE* "out" variable. Here's how you can call the function to obtain the token handle.

// create an empty HANDLE object to receive the token handle let hToken = new HANDLE(); // ask Windows for the token handle if (Advapi32.OpenThreadToken(Advapi32.GetCurrentThread(), TOKEN_QUERY, 1, hToken)) { // Success - hToken now contains the thread token }

You can use the same technique when an HWND* out parameter is used. In that case, use new HWND() to create the empty window handle object that will hold the result.

Defining C struct types

Many Windows API functions use C "struct" (structure) types as arguments (or, more commonly, pointers to struct types). For example, the GetWindowRect() API function takes a pointer to a RECT structure, which has elements for the corner coordinates of a rectangle:

let User32 = dllImport.bind("User32.dll", "BOOL WINAPI GetWindowRect(HWND, struct RECT *)");

If you just run that bind() call without doing any preparation first, it'll fail with an error, because "struct RECT" isn't a built-in type. You have to define it before you can use it in a function definition.

There is, naturally, a dllImport() call to do this: define(). As with the function declaration itself, you use the standard C syntax for declaring a struct type:

dllImport.define("struct RECT { LONG left; LONG top; LONG right; LONG bottom; }");

The define() method doesn't return anything. It just parses the type definition and adds it to an internal table of type names inside the dllImport object. Once the type is entered into the internal table, you can use it in subsequent bind() calls.

The define() call above uses almost the same syntax that Windows SDK documentation uses for RECT. But they actually take this one step further, by defining a couple of "typedef" types for the structure. typedef is a C language feauture that creates a global alias for a type. It simply adds a new global name for the type that you can use in other places, such as in function declarations. Here's a revision to the above, using the full typedef that the SDK uses:

dllImport.define("typedef struct _RECT { LONG left; LONG top; LONG right; LONG bottom; } RECT, *PRECT");

You'll see that naming pattern throughout the Windows SDK headers: "typedef struct _FOO { ... } FOO, *PFOO". That actually defines three related types: the struct name itself, with an underscore prefix on the name (struct _FOO), a global alias with the plain name (FOO), and a global "pointer" alias with the "P" prefix (PFOO). There's almost always a fourth alias as well, LPFOO. The "LP" prefix stands for "long pointer". Long pointers are holdovers from the 16-bit Windows 3 era - they're exactly the same as regular pointers now - but the Windows headers still use them everywhere. So let's add the "LP" to our typedef to make it complete:

dllImport.define("typedef struct _RECT { LONG left; LONG top; LONG right; LONG bottom; } RECT, *PRECT, *LPRECT");

So there you have it: that's the form of the typedef you'll need to enter for virtually every struct you'll need in a Windows API call. With this definition in place, the bind() call for GetWindowRect() that we showed earlier will finally work properly.

Using back-quote strings: For long struct definitions, you might prefer to use Javascript's "back-quote string" style, which lets you spread out a string across multiple lines of text. That lets you paste a multi-line struct from the SDK without having to compress it into one line, which makes the result a lot more readable.

dllImport.define(` typedef struct _STARTUPINFOW { DWORD cb; LPWSTR lpReserved; LPWSTR lpDesktop; LPWSTR lpTitle; DWORD dwX; DWORD dwY; DWORD dwXSize; DWORD dwYSize; DWORD dwXCountChars; DWORD dwYCountChars; DWORD dwFillAttribute; DWORD dwFlags; WORD wShowWindow; WORD cbReserved2; LPBYTE lpReserved2; HANDLE hStdInput; HANDLE hStdOutput; HANDLE hStdError; } STARTUPINFOW, *LPSTARTUPINFOW; `);

Using structs as "out" parameters

As mentioned above in Out parameters, many Windows API functions take pointers to structs as parameters, so that they can return information to the caller by writing into the struct.

Whenever a DLL function requires a struct or a pointer to a struct as a parameter, you can directly pass a Javascript object as the argument, and the DLL importer will create a temporary copy of the native struct from the Javascript object's properties. This is described below under Automatic type conversions. However, this is a purely one-way process: you can pass information to a DLL function this way, but you can't get any information back from the function. The reason is that the DLL function doesn't get a direct reference to your Javascript object - it just gets that temporary copy of the native struct that the DLL importer creates. Anything that the DLL function writes into the struct go into the temporary struct, which is lost after the function returns.

The solution is exactly the same as the solution described earlier for simple types like "int pointer" parameters. You use dllImport.create() to explicitly create your own copy of the native struct, and you pass that struct to the DLL function.

let rc = dllImport.create("RECT"); User32.GetWindowRect(hwnd, rc);

As with our earlier example of creating a native "int" variable, the RECT struct that we created here is represented as a Javascript object that contains the native data structure. You can access the struct's named field as though they were Javascript properties of the object:

alert("The window is at (" + rc.left + ", " + rc.top" + "), (" + rc.right + ", " + rc.bottom + ")");

Note that there's no .value property of a struct type, as there is for a basic type like "int". Instead, you use the named struct elements.

Native pointer types

Structs often contain pointer types. If you encounter one of these, you can access pointer fields just like anything else. The type system will give you a Javascript object representing the native pointer contained in the field. To access the struct or basic type value that the pointer points to, use the .at property:

let x = p.at; // x now accesses the native object that p points to

You can also explicitly create a native pointer variable, just like any other native type:

let p = dllImport.create("int*");

When you first create a pointer type, it will be initialized with a null pointer, so attempting to get the underlying value with .at will throw an error. You can assign another pointer value to it by setting its value:

p.value = otherPointer.value; // assign the value from some other native pointer

You can get the address of a native object for storing in a native pointer variable using the NativeObject.addressOf() function:

let i = dllImport.create("int"); p.value = NativeObject.addressOf(i);

To protect against memory corruption and application crashes, you can only assign a pointer value from a pointer to the same type. The one exception is the special "void" pointer ("void*"), which C uses as a generic pointer to anything. In keeping with C's general philosophy that it should give you enough rope to hang yourself, the native "void*" type relaxes the normal type restrictions and lets you assign unlike pointer types. This goes both directions; you can assign anything to a void* pointer, and you can assign any other pointer type from a void* pointer. So in special situations when the normal type rules are too restrictive, you can say "I know what I'm doing here, just do as I say" by laundering your pointers through a void* intermediary. If none of this makes any sense to you, it's probably just as well; it's usually better to avoid this sort of thing anyway.

For more on this native pointer container type, see NativePointer objects.

Native object memory management

If you've done any programming in C or C++, you know that those languages require you to do all of your memory management explicitly: you have write code to create and destroy every object you use. Javascript, in contrast, handles most of that automatically.

The DLL import approach to managing native objects is somewhere between the low-level details of C/C++ and the completely automatic, invisible garbage collection in Javascript.

When you create a native object with dllImport.create(), you're really creating two objects: a Javascript NativeObject object, and a block of memory containing the native type named in the create() call. The Javascript object is managed automatically, just like any other Javascript object. The native memory block is also managed automatically: it will remain valid as long as there are any references to it from Javascript objects. So you don't have to do any special manual memory management for objects you create with dllImport.create().

If you pass an object you create with dllImport.create() to native code in a DLL, be sure that you understand how the native callee will use that memory. In particular, pay attention to whether or not the callee expects the memory to stay valid after the call returns. If the callee retains the pointer and expects it to remain valid, you'll have to ensure that the Javascript NativeObject that refers to the native memory stays "alive", so that Javascript's garbage collector doesn't delete it. Remember, the native memory underlying a NativeObject that you create with dllImport.create() stays valid as long as its NativeObject stays alive. To keep a Javascript object alive, all you have to do is reference it from a global variable.

When you get a NativeObject from a DLL call - for example, if a DLL function returns a pointer to a struct - the NativeObject is managed automatically like any other Javascript object. However, the underlying native memory is under the control of the native DLL code. This has some important implications.

Argument type conversions

When you pass arguments from Javascript to a DLL function, the DLL importer automatically converts the Javascript values to the native types listed in the function declaration. Here's a summary of the conversion rules.

Native parameter typeJavascript typeConversion rule
bool Any The Javascript value is converted to boolean using the standard Javascript implicit type conversions. If the result is true, the native C true value is used (equivalent to integer value 1). If the result is false, the native C false value is used (equivalent to integer value 0).
char, INT8 Any The Javascript value is converted to a number using the standard Javascript implicit conversions. Any fractional part is discarded (not rounded). If the final value is out of the valid range for char (-128 to +127), an error is thrown.
unsigned char, UINT8 Any The Javascript value is converted to a number using the standard Javascript implicit conversions. Any fractional part is discarded (not rounded). If the final value is out of the valid range for unsigned char (0 to 255), an error is thrown.
short, INT16 Any The Javascript value is converted to a number using the standard Javascript implicit conversions. Any fractional part is discarded (not rounded). If the final value is out of the valid range for short (-32768 to +32767), an error is thrown.
unsigned short, UINT16 Any The Javascript value is converted to a number using the standard Javascript implicit conversions. Any fractional part is discarded (not rounded). If the final value is out of the valid range for unsigned short (0 to 65535), an error is thrown.
int, long, INT32 Any The Javascript value is converted to a number using the standard Javascript implicit conversions. Any fractional part is discarded (not rounded). If the final value is out of the valid range for a 32-bit int (-2147483648 to +2147483647), an error is thrown.
unsigned int, unsigned long, UINT32 Any The Javascript value is converted to a number using the standard Javascript implicit conversions. Any fractional part is discarded (not rounded). If the final value is out of the valid range for a 32-bit unsigned int (0 to 4294967295), an error is thrown.
__int64, INT64 Number Any fractional part of the number is discarded (not rounded), and the value is converted to a 64-bit native integer. If the value is out of range (-9223372036854775808 to +9223372036854775807), an error is thrown.
Int64 The stored 64-bit value is used.
Int64 If the unsigned value is above the maximum value for a signed 64-bit integer (9223372036854775807), an error is thrown. Otherwise, the stored value is simply treated as signed.
String If the unsigned value is above the maximum value for a signed 64-bit integer (9223372036854775807), an error is thrown. Otherwise, the stored value is simply treated as signed.
Other The Javascript value is converted to a string using the standard Javascript implicit conversions, then the string is parsed as a 64-bit integer value, using the same rules as the Int64 constructor.
unsigned __int64, UINT64 Number Any fractional part of the number is discarded (not rounded), and the value is converted to a 64-bit native integer. If the value is out of range (-9223372036854775808 to +9223372036854775807), an error is thrown.
Int64 The stored 64-bit value is used.
Int64 If the unsigned value is above the maximum value for a signed 64-bit integer (9223372036854775807), an error is thrown. Otherwise, the stored value is simply treated as signed.
String If the unsigned value is above the maximum value for a signed 64-bit integer (9223372036854775807), an error is thrown. Otherwise, the stored value is simply treated as signed.
Other The Javascript value is converted to a string using the standard Javascript implicit conversions, then the string is parsed as a 64-bit integer value, using the same rules as the Int64 constructor.
float Any The Javascript value is converted to a Number using the standard Javascript implicit type conversion rules, then that value is reinterpreted as a single-precision (32-bit) floating point value. If the value is outside of the range that a float can represent (-3.402823466e+38F to +3.402823466e+38F), an error is thrown. Javascript numbers store more precision than a float, so the extra precision is lost in the conversion, but this doesn't cause an error.
double Any The Javascript value is converted to a Number using the standard Javascript implicit type conversion rules. The internal storage format of a Javascript Number is identical to a native double, so the value is just copied directly to the native callee. There's no possibility of overflow or loss of precision for this conversion.
INT_PTR, SSIZE_T, ptrdiff_t Any These native types are equivalent to INT32 when running the 32-bit version of PinballY, and equivalent to INT64 when running the 64-bit PinballY. The conversions are handled identically to the corresponding INTxx type, depending on which PinballY version you're running.
UINT_PTR, SIZE_T Any These native types are equivalent to UINT32 when running the 32-bit version of PinballY, and equivalent to UINT64 when running the 64-bit PinballY. The conversions are handled identically to the corresponding UINTxx type, depending on which PinballY version you're running.
HANDLE null, undefined A native NULL handle value is used (equivalent to a native null pointer)
Number The numeric value is reinterpreted as a native HANDLE value. No error checking is performed to determine if the resulting handle is valid. A zero numeric value is equivalent to a native NULL handle value, so 0 has the same meaning as null or undefined.
HANDLE object The native handle value stored in the object is used.
LPSTR, LPCSTR ArrayBuffer, Int8Array, Uint8Array The callee receives a direct pointer to the byte array underlying the Javascript array object. Any changes that the callee makes to the contents of the array will be visible to the Javascript code on return.
Other The value is converted to a String using the standard Javascript implicit type conversion rules. The string is then converted to a single-byte character string, using the local system's localized ANSI character set, a null terminator byte is added, and the result is stored in a temporary memory buffer. The callee receives a pointer to the temporary memory buffer. Note that Javascript uses Unicode strings internally, whereas an LPSTR or LPCSTR uses single-byte ANSI characters. Some Unicode characters from the source string might not be representable in the ANSI character set. Any characters that can't be represented are replaced with a "default" character representing an unknown code point, usually drawn on screen as an empty rectangle.
LPTSTR, LPCTSTR, LPWSTR, LPTWSTR ArrayBuffer, Int16Array, Uint16Array The callee receives a direct pointer to the byte or integer array underlying the Javascript array object. Any changes that the callee makes to the contents of the array will be visible to the Javascript code on return.
Other The value is converted to a String using the standard Javascript implicit type conversion rules. The string is then copied into a temporary memory buffer, with a null terminator added. LPTSTR, LPCTSTR, LPWSTR, and LPCWSTR are native Unicode string types, and Javascript uses Unicode strings internally, so no character set conversion is necessary.
Pointer to struct or union Object
  • A copy of the native struct is created in a temporary memory area
  • All bytes of the struct are set to zero
  • If the struct has an element named exactly cbSize, and it's any 16-, 32-, or 64-bit integer type, the size in bytes of the native struct is stored in the element
  • For each property of the Javascript object passed as the argument: if the property name exactly matches the name of an element of the native struct, the property value is converted to that element's native type (using the same rules outlined in the rest of this table) and then stored in the element's slot in the temporary copy of the struct
  • A pointer to the temporary struct is passed to the callee
  • The temporary struct is deleted when the callee returns
Array If a Javascript array is passed as a parameter requiring a native pointer type, the rules for native array arguments below are used.
NativeObject If the NativeObject contains a struct of the exact same type, a pointer to the native struct contained in the object is passed to the callee. Otherwise an error is thrown.
Other An error is thrown
Inline struct or union Object Same as for a pointer to struct or union above, except that in 32-bit mode, or if the struct's physical memory layout fits within 8 bytes, the temporary copy of the native struct is allocated inline on the stack, rather than being passed as a pointer. Microsoft's 64-bit calling conventions require passing even nominally inline structs as pointers if they're over 8 bytes.
NativeObject The contents of the native object are copied to a temporary memory area, either inline with the stack arguments (in 32-bit mode or if the struct fits within 8 bytes), or separately, with a pointer to the temp struct passed in the arguments. An error is thrown if the native object's type doesn't match the parameter type.
Other An error is thrown
Array Array A temporary native array is created, using the native type defined for the array parameter, and the number of elements in the Javascript array. The Javascript array elements are then copied into the temporary array, using the same conversion rules as described in the rest of this table to convert the elements to native types. The callee receives a pointer to the temporary native array. The temporary array is deleted when the callee returns.
Typed Array If one of the Javascript built-in Typed Array types (Int8Array, Uint16Array, etc) is passed to a native array argument, and the type of the array exactly matches the type of the native argument, the callee receives a pointer to the underlying native array within the Javascript object. Otherwise an error is thrown.
NativeObject If the native object contains an array of the same type, the native callee receives a pointer to the native array data within the object. An error is thrown if the native object isn't a matching array type.
Pointer Array A temporary native array is created, using the underlying native type of the pointer, with the same number of elements as the Javascript array argument. For example, if the argument type is "int *" (pointer to int), and a Javascript array with 10 elements is passed as the argument, as 10-element int array is created as the temporary array. The Javascript array elements are then copied into the temporary array, using the conversion rules described in this table to convert the individual elements to the native types referred to by the pointer. The callee receives a pointer to the temporary native array. The temporary array is deleted when the callee returns.
Typed Array If one of the Javascript built-in Typed Array types (Int8Array, Uint16Array, etc) is passed to a native array argument, and the type of the array exactly matches the type referred to by the pointer, the callee receives a pointer to the underlying native array within the Javascript object. Otherwise an error is thrown. For example, a UINT32* argument requires a Uint32Array typed array.
NativeObject The handling depends on the type of native data contained in the object:
  • If it's a pointer with a compatible type, the pointer value is passed directly to the callee. For example, if the parameter is declared as a UINT32*, and the native object also contains a UINT32* value, that pointer is passed to the callee.
  • If the native data is a value of the same type that the pointer refers to (e.g., the parameter is an INT32*, and the native object value is an INT32), a pointer to the native value in memory is passed. This allows passing values for OUT parameters.
  • Otherwise an error is thrown.
Other A temporary variable is created with the native type that the pointer refers to. For example, if the parameter is an INT32*, a temporary INT32 variable is created. The Javascript value is then converted into that type and stored in the temporary variable, using the rules described in this table for the relevant types. A pointer to the temporary variable is passed to the callee. The temporary variable is deleted when the callee returns.
Function pointer Function (named or anonymous/closure) A callable native-code function representing the Javascript function is created, and a pointer to this native function is passed to the callee. The callee can use the native function pointer to invoke the Javascript function.
Other An error is thrown

Standard type conversions: The "standard implicit type conversions" mentioned several times in the list above are the ones that Javascript normally performs when type conversions are called for when working entirely in the Javascript domain. For example, using any type of value in a Javascript arithmetic expression implicitly converts the value to a Number. You can find more about the standard conversion rules in any Javascript language reference.

In addition to the standard conversions, the DLL importer also converts any Int64 or Uint64 value to a Number as part of any of the conversions listed above that implicitly convert to Number at any point. In this case, an error is thrown if the 64-bit integer value is outside of the range that can be exactly represented in a Javascript Number (-9007199254740991 to +9007199254740991).

Objects: As described in the table, a Javascript object can be passed to a native callee when a struct pointer is required. This is accomplished by creating a temporary copy of the native struct, populating it from the Javascript object's properties, and passing the callee a pointer to the temporary struct.

Note that this is a strictly one-way data transfer, because of the temporary nature of the struct passed to the native callee. Any changes that the callee makes to the struct are lost on return, since the temporary struct is deleted after the native code returns. If you need to receive information back from the native function through the struct, you must explicitly create your own copy of the native struct using dllImport.create(), and pass that to the DLL function. See Out parameters.

Strings: The DLL importer has some special provisions for string handling. See below for details.

More details on ArrayBuffer: The Javascript ArrayBuffer type lets you create a native byte array and manipulate it from Javascript. It's an excellent "type of last resort" for special cases that the DLL Import system can't handle with its automatic type conversions. You can pass an ArrayBuffer object for any native function argument that requires a pointer to any type. The DLL import layer will pass the underlying memory pointer directly to the native code. It's up to you to prepare the bytes contained in the ArrayBuffer in the appropriate format for the callee before the call, and/or to interpret the results left in the buffer on return. If the callee will write into the buffer, it's up to you to make sure that the buffer is large enough for what the callee expects.

ArrayBuffer should only be used when you can't use something more structured, like a native type created with dllImport.create() (see Out parameters) or via one of the automatic conversions listed above. You can use ArrayBuffer in cases where it's not possible (or not convenient) to express the type of the function argument as a C struct type. For most Windows APIs, this isn't a concern, as the Windows APIs are designed around C callers to start with. But other DLLs might have different data formats that don't translate well to C terms.

Strings

Javascript makes it easy to work with character strings. A String in Javascript is a first-class object that you can operate on as a unit, without having to worry about the details of how the text inside is actually stored. C code and machine code aren't nearly as high-level. To them, a string is just an array of bytes (or 16-bit integers, in the case of a Unicode) string). The Windows DLL conventions are all designed around the C way of doing things, so DLL interfaces think in terms of those low-level "character array" strings used in C rather than the high-level "objects" in Javascript.

The DLL importer tries to smooth over this mismatch for you by converting automatically between the two views of strings whenever possible. Here's a summary of the type conversions that the DLL importer applies.

Native typeJavascript valueConversion
LPSTR, LPCSTR String The Javascript string is converted to single-byte characters, using the local ANSI character set, a null terminator is added, and the characters are copied into a native byte array. A pointer to the byte array is then passed to the native code. Any changes to the buffer made in the native code are not returned to Javascript.
LPTSTR, LPCTSTR, LPWSTR, LPCWSTR String The Javascript string is converted to Unicode characters, a null terminator is added, and the characters are copied into a native wide-character (16-bit int) array. A pointer to the character array is passed to the native code. Changes made to the buffer in the native code are not returned to Javascript.
LPSTR, LPCSTR Int8Array, Uint8Array A pointer to the object's internal byte array is passed directly to the native code, with no copying. Since the native code has access to the Javascript buffer, any changes that the native callee makes to the buffer are visible to the Javascript code on return. Note that LPCSTR means that the callee promises not to change the buffer (the "C" is for "constant"), but there's no enforcement, so the buffer could be changed if the callee does something wrong.
LPSTR, LPCSTR Other typed arrays Invalid
LPTSTR, LPCTSTR, LPWSTR, LPCWSTR Int16Array, Uint16Array A pointer to the object's internal int array is passed directly to the native code, with no copying. Any changes made in the native callee are visible to the Javascript caller.
LPTSTR, LPCTSTR, LPWSTR, LPCWSTR Other typed arrays Invalid
All LPxxSTR types Any other type For any Javascript type other than the ones listed above, the Javascript value is converted to a string (using the normal Javascript conversion rules), the string is null-terminated and copied into a buffer of the appropriate character type, and the callee is passed a pointer to the buffer. Changes made to the buffer are not returned to Javascript.
char*, const char*, unsigned char*, const unsigned char*, INT8*, const INT8*, UINT8*, const UINT8* String The Javascript string is converted to an 8-bit character string array, null-terminated, and copied into a buffer. The address of the buffer is passed to the native code. Changes made to the buffer are not returned to Javascript.
unsigned char*, const unsigned char*, UINT8*, const UINT8* Uint8Array A pointer to the Uint8Array's byte array is passed directly to the native code, with no copying. Changes that the callee makes to the array are visible to the calling Javscript code on return.
unsigned short*, const unsigned short*, UINT16*, const UINT16* String The Javascript string is converted to a null-terminated Unicode character array and copied into a buffer. The address of the buffer is passed to the native callee. Changes to the buffer are not returned to Javascript.
unsigned short*, const unsigned short*, UINT16*, const UINT16* Uint16Array A pointer to the Uint16Array's byte array is passed directly to the native code, with no copying.

Convenience functions for Int8Array, Uint8Array, Int16Array, and Uint16Array: The PinballY system scripts provide you with some extra functions to make it easier to use these array types to transfer strings to and from DLL functions.

You can generally pass Javascript strings directly to any native function that requires a pointer to a string or string buffer, but that's only good for getting a string to a DLL function. Many native DLL functions return string values by storing them in buffers you provide. For that, you need Int8Array, Uint8Array, Int16Array, or Uint16Array, since these provide the buffers that the native code needs, and let you retrieve the results in Javascript.

The only snag is that typed arrays don't act like ordinary strings in Javascript. So these types are great for getting data back from DLL functions, but once you have data back in one of these objects, you'll usually want to convert it to a Javascript string. Similarly, you might need to create one of the array objects from a string. The convenience functions let you do these conversions in one step.

Native string handling concepts: If you're not familiar with the way that C and native DLLs handle strings, some of the key concepts are described below. These might be helpful to understand if you run into any weird situations that the summary above doesn't explain properly.

Typed Arrays

Javascript typed arrays can be used any time a native interface calls for an array of or pointer to the corresponding native type.

A typed array argument must always exactly match the native type declared for the function parameter. The correspondences between native types and Javascript typed array types are listed below.

C pointers and arrays are closely related, and can be used interchangeably in function arguments. For the sake of brevity, we only list pointer types in the table, using the C "*" pointer notation, but you can use the same Javascript array types with C array arguments of the corresponding types. For example, when we list unsigned short *p in the table, you can also use the same typed array type with an unsigned short[] or unsigned short p[size] function parameter.

Native typeJavascript value
char*, const char*, INT8*, const INT8* Int8Array
unsigned char*, const unsigned char*, UINT8*, const UINT8* Uint8Array
short*, const short*, INT16*, const INT16* Int16Array
unsigned short*, const unsigned short*, UINT16*, const UINT16* Uint16Array
long*, const long*, INT32*, const INT32* Int32Array
unsigned long*, const unsigned long*, UINT32*, const UINT32* Uint32Array
float*, const float* Float32Array
double*, const double* Float64Array

Getting the size of a native type

When calling native DLL functions, you'll sometimes need to determine the size of a native type in order to pass the right parameters to the function. The dllImport object has a sizeof() method that helps with this.

To determine the size of a type, call dllImport.sizeof("typename"), where typename is the name of the native type whose size you want to obtain. The type name uses the same C/C++ syntax as used in dllImport.define() and dllImport.bind(). It can be the name of a basic primitive C type (char, short, int, long, etc), the name of one of the Windows object types (HANDLE, HWND), or the name of a struct, union, or typedef previously defined with dllImport.define().

The return value is an integer giving the number of bytes the type uses on the current platform. Note that some types are different sizes on 32-bit and 64-bit Windows platforms.

Examples:

Auto "cbSize" struct elements

It's very common in the Windows API for structs passed as arguments to include a member named cbSize that indicates the size (in bytes) of the struct in memory. Many Windows APIs use this as a sort of sanity check that the caller is passing in a valid memory pointer, and sometimes to detect which version of a struct definition the caller is using. When a cbSize element is present, the caller almost always has to initialize it before using the struct in an API call; most Windows API calls that involve a struct with a cbSize field will return an error if cbSize isn't filled in properly.

This is such a common convention in the Windows APIs that the DLL importer has special handling for it. Whenever you call a DLL function that takes a struct (or a pointer to a struct) as an argument, the DLL importer will automatically fill in the struct's cbSize field with the size of the containing struct, if all of the following are true:

If all of those conditions are true, the DLL importer will automatically fill in cbSize in the native struct with the byte size of the native struct. You can easily override this automatic behavior, in case it doesn't do what you want for a particular function, by either changing the name of the native struct element to anything other than "cbSize", or by passing in a value explicitly for the cbSize property in the Javascript object used for the struct argument.

Windows HANDLE objects

Many API calls in Windows work with HANDLE objects of one type or another: window handles, bitmap handles, etc. These are so widely used that the DLL importer treats handles as a special type. Type names for most of the standard Windows handle types are pre-defined, and you can always create new ones with a typedef:

dllImport.define("typedef HANDLE HWHATEVER");

When a HANDLE value is returned from a native function, the importer gives you a special native-code Javascript object to represent the handle value. You can then pass this object back to any DLL call that takes a handle value as a parameter.

The reason the importer uses the special native-code object is that HANDLE types are basically native pointers, which are 64-bit values on 64-bit Windows versions. Javascript doesn't have a primitive type that can represent this properly. Its closest approximation is the Number type, which actually does use 64 bits, but those bits have to be formatted in a certain way (specifically, in the double-precision floating point format). The native-code HANDLE object ensures that the system handle value is stored properly.

See HANDLE objects for more details.

Callback functions

Many Windows APIs take "callback" function parameters. A callback is simply a caller-provided function that the API calls back into (thus the name) during the API operation. This is most often used for enumeration functions that call the callback once for each item in a collection, such as child windows of a given window.

// C++ example - typical C++ code to get a list of child windows static BOOL CALLBACK my_callback_function(HWND hwndChild, LPARAM lParam) { // recover 'this' from the lParam MyClass *self = reinterpret_cast<MyClass*>(lParam); // do something with the child window handle... self->process_child(hwndChild); } void MyClass::my_calling_function(HWND hwndParent) { EnumChildWindows(hwndParent, &my_callback_function, reinterpret_cast<LPARAM>(this)); }

This concept is hardly foreign to Javascript. In fact, it's so widely used by Javascript built-ins and frameworks that some people think Javascript invented it! You see this coding style used in the built-in Javascript array type, for example:

var array1 = [1, 15, 32, 17, 18, 19, 12]; function isEven(x) { return x/2 == 0; } var evens = array1.filter(isEven); // named function var odds = array1.filter(function(x) { return x/2 != 0; }); // anonymous function var doubled = array1.map(x => x*2); // "arrow" function

Callbacks in Javascript are usually written with anonymous functions (a/k/a closures or lambdas), as in the last two examples above. C/C++ callbacks usually use named functions, as in the first example (although modern C++ also allows syntax similar to Javascript closures). All of these formats are just syntactic variations, though; they all have the same effect of providing a function reference to the called function, which calls it back to carry out some work.

The DLL importer makes it easy to use callbacks when required in native APIs. You just use the standard C syntax to define the API function that takes a function pointer parameter, then you call it with a Javascript function for the function pointer argument. You can use named functions or closures (lambdas/anonymous functions).

dllImport.define("typedef BOOL (CALLBACK *WNDENUMPROC)(HWND hwnd, LPARAM lParam)"); let EnumWindows = dllImport.bind("User32.dll", "BOOL WINAPI EnumWindows(WNDENUMPROC lpEnumFunc, LPARAM lParam)"); // make an array of all top-level window handles let windows = []; EnumWindows((hwnd) => windows.push(hwnd), 0);

The DLL importer provides the necessary plumbing that turns the Javascript function reference into a native function pointer that the native code in the DLL can call directly. That in turn invokes your Javascript function. As when calling from Javascript into DLL native code, the DLL importer automatically converts argument values when calling back from the DLL into Javascript, so your function works just like it would if it were being called directly from Javascript code.

LPARAM and LPVOID "context" pointers: Almost every Windows API that takes a callback function pointer argument has an additional LPARAM or LPVOID parameter at the end. The Windows headers usually give this parameter the uninformative name "lParam", so you have to read the documentation to figure out that its purpose is just to let you pass some extra information to your own callback function. Why would you need that? Because it lets you use the same callback function in different places in the code. The extra information in the LPARAM or LPVOID parameter tells you the current context in which it's being invoked. In fact, these parameters are usually called "context" arguments.

You see context arguments all the time in C callback code, but you rarely see them in Javascript. The reason is that Javascript functions have their own sort of built-in context, because a Javascript anonymous function can access all of the local variables in its enclosing scope. That's handled behind the scenes by the Javascript engine, so it happens without any special coding on your part.

When you pass a Javascript function to a native DLL call, the Javascript function has access to all of its local variable context, exactly as normal. That makes it unnecessary to pass anything at all for the LPARAM or LPVOID parameter to a Windows API that will call the Javascript callback. You can just pass 0 for these arguments. But if you do need to use the context argument for some reason, you can pass any integer value, and it'll get passed back to your callback function in the corresponding LPARAM or LPVOID parameter.

C's wacky function pointer syntax: The C syntax for defining a function pointer is pretty obtuse. Fortunately, you can just copy and paste the definitions for most of the common Windows callback types from the SDK documentation, so you shouldn't have to become an expert at the C syntax. For reference, though, here's the basic C function pointer declaration syntax. It's a little bizarre, but in most cases you should be able to just stick to this formula:

typedef return-type ( calling-convention *function-name )( arguments )

There actually is a somewhat more readable alternative that some people prefer. The trick is to do the declaration in two steps. First, you define a typedef for the type of the function itself. This is easy because it looks almost exactly like a regular function definition, the only difference being the addition of the "typedef" keyword at the start. That makes it a type definition instead of an actual function declaration. Then you define a second typedef that's just a pointer to that first type. That's also easy, because it uses the ordinary TYPE* syntax. Here's how we could have done that with our EnumWindows() callback:

typedef BOOL CALLBACK WndEnumProc(HWND, LPARAM); // step 1: define the function type typedef WndEnumProc* WNDENUMPROC; // step 2: pointer to the function type

COM and OLE objects

The DLL importer has some support from COM interfaces as well.

The importer pre-defines for the following COM interfaces, so you don't have to define these on your own:

For any other COM interfaces you wish to use, you'll have to define them, using the dllImport.define() function. Unlike most of the definitions we've seen so far, interface definitions don't use standard C language syntax. Instead, we use a syntax that's similar to the syntax used in Microsoft's IDL (Interface Definition Language) files. The basic syntax is:

interface name 'GUID' : baseInterface { // C-style function definitions };

The GUID is specified in the standard Microsoft hex format, with hyphens between digit groups; you can optionally use curly braces around the GUID (inside the quote marks), but they're just for decoration and have no other effect. Every COM interface has a GUID, defined by Microsoft (or whoever else defined the interface). You can't just make these up as they're permanent features of the interface. You can find the correct GUID for any standard Windows interface in the IDL file in the SDK where the interface is defined; look for the uuid(...) definition preceding the interface statement.

The base interface is the name of a previously defined interface that serves as the "base class" of the interface. With the single exception IUnknown (which is pre-defined by the DLL importer, so you don't have to define it yourself), all COM interfaces are based on some other interface, and IUnknown is always the ultimate base interface. An interface automatically "inherits" all of the functions in its base interface.

The function definitions within the interface use standard C syntax.

For example, here's how you'd define the standard Windows interface IPersist:

dllImport.define(` interface IPersist '0000010c-0000-0000-C000-000000000046' : IUnknown { HRESULT GetClassID(CLSID *pClassID); };`);

The C parser also accepts the MIDL_INTERFACE syntax used in Windows SDK .idl files. This makes it fairly easy to copy and paste interface definitions from the .idl files directly into your Javascript code, with little or no hand-editing, which can make it a lot faster to import a bunch of interfaces if you trying to get something complex working.

dllImport.define(` MIDL_INTERFACE("0000010c-0000-0000-C000-000000000046") interface IPersist : public IUnknown { public: virtual HRESULT STDMETHODCALL GetClassID(CLSID *pClassID) = 0; };`);

The MIDL_INTERFACE syntax is a little more verbose, since it adds the C++ keywords like "public", "virtual", and the "=0" pure-virtual qualifier. All of these extra keywords are accepted, but are simply ignored, as they're all implied in an interface anyway.

Note that I like to use the back-quote (`) notation for COM interface definitions, because they tend to be too long to pack onto one line. But of course, this isn't required; the definition is just a string, and you can use any string delimiters you prefer.

As with any C function definition, you can specify a calling convention for each function in an interface, but it must always be STDMETHODCALLTYPE. This is the default for all interface functions, so you can safely omit it, and it's an error to specify any other convention.

The order of the functions listed in an interface definition is critical. It must exactly match the definition in the IDL file in the Microsoft SDK where the interface is officially defined. A live COM object instance is essentially an array of function pointers, so the only way that Javascript knows which function you want to call is by matching its position in the interface definition to the position in the pointer array. The caller and the object have to agree on the order of the functions, which all contained in the IDL layout.

Pointers to interfaces: Once you define an "interface" type, it works a lot like a "struct" or "union" type in terms of referencing it from other type definitions. For example, you can create a pointer to an interface using syntax like this:

let pUnk = dllImport.create("interface IUnknown*");

The same syntax works for typedef statements. It's common in the Windows SDKs to create the usual raft of type alias for interface pointers, which you can replicate with dllImport.define() using typedefs:

dllImport.define("typedef interface IPersist IPersist, *LPPERSIST");

The DLL importer has special handling for interface OUT pointers in function arguments. COM interfaces that return an interface pointer to a caller always do so via a parameter that's defined as a pointer to a pointer to an interface, such as IMoniker**. The double asterisks mean "pointer to pointer". This is often just void** (pointer to pointer to void) rather than a specific interface type. You see the void** type used in QueryInterface, for example:

HRESULT QueryInterface(REFIID riid, void **ppvInterface);

Whenever a COM interface requires a pointer-to-pointer variable, be it a particular Interface** or a generic void**, the DLL importer lets you pass in a plain Interface* variable. The importer automatically passes the address of your pointer variable in these cases, as required by the interface definition. This makes it easier to call functions like QueryInterface and CoCreateInstance, since you don't have to explicitly add any "AddressOf" calls.

let p = dllImport.create("ITest*"); i.QueryInterface(IID_ITest, p);

IID_PPV_ARGS: In the case of QueryInterface, there's an even quicker short-hand format you can use. Whenever a function interface has a pair of parameters of the form REFIID, Interface** or REFIID, void**, you can pass an interface pointer for the pair of arguments. So we could rewrite the call above like this:

i.QueryInterface(p);

We call this the IID_PPV_ARGS pattern because that's the name of a macro in the Windows COM headers that does basically the same thing in C++ code. It stands for "interface ID, pointer to pointer to void arguments". Microsoft created this macro because they noticed that this particular argument combination occurs an awful lot in COM code; people were getting tired of writing it out the long way every time. In Javascript, this works by matching the interface pointer argument to the GUID argument, and then matching it again to the subsequent Interface** or void** argument. This works because a COMPointer object (the Javascript object type that represents a native COM interface pointer) knows its own interface GUID, which is what's required for the REFIID argument.

Namespaces

The C type parser has a feature similar to C++ namespaces. This is primarily intended for people writing modules that they want to share with other people, since it provides a way to avoid type name conflicts when your shared module is used together with other shared modules written by other people. If each shared module defines its types within a namespace, and each namespace is given a sufficiently distinctive name, any two modules should be able to work together without type naming conflicts.

When you define a struct or other type via dllImport.define(), it goes into a global name table. That means that you can only define one struct type for a given name, across all scripts. For most Windows SDK headers, that's fine, because the SDK takes pains to give everything a unique name. If you're mixing custom DLLs from different sources, though, you might find that others authors aren't always so careful about unique naming, so you might run into conflicts, where two DLLs use the same struct name for different purposes.

The easy way to work around this is to just rename one of the types. The names of structs and other types aren't part of the DLL interfaces; the only names that are actually part of DLL interfaces are the function names. So you can use whatever name you like for a struct type. It's easier to keep things straight if you stick to the same names used in the DLL's header files or documentation, of course, but there's nothing else compelling you to use the original names.

A more general way to handle name conflicts is to use "namespaces". A namespace is essentially a prefix that's applied to each type name in a group. You create a namespace using the namespace keyword, followed by an open brace. Every symbol defined up to the next closing brace is part of that namespace.

dll.define(` namespace MyDll { struct Color { UINT8 r; UINT 8 g; UINT8 b; }; struct Brush { int width; struct Color color; }; } `);

Each name defined within the braces of a namespace statement is implicitly prefixed with the namespace name and a "::" delimiter, so in the example above, struct Color is actually defined as struct MyDll::Color, and struct Brush is actually struct MyDll::Brush. When you refer to these types in your Javascript code, you must use the fully qualified name, with the "namespace::" prefix:

// create a Color struct from the MyDll namespace let c = dllImport.create("struct MyDll::Color");

Within a namespace { } section, whenever you reference a type (as opposed to defining a new type), the parser looks for an existing type that matches the name. It starts by assuming that you're talking about the current namespace, but if no matching type is found there, it looks at the enclosing namespace(s). So in the example above, the reference to struct Color within the definition of struct Brush is matched to the previously defined struct MyDll::Color. If there hadn't been a struct Brush within the namespace, though, the parser would have looked for a global struct Brush, defined outside of any namespace. Let's elaborate the example to show how this works:

dll.define(` struct Rect { int left; int top; int right; int bottom }; namespace MyDll { struct Color { UINT8 r; UINT 8 g; UINT8 b; }; struct Brush { int width; struct Color color; }; void FillRect(struct Rect *r, struct Brush *br); } `);

In the definition of the function FillRect(), struct Brush refers to struct MyDll::Brush, as in the earlier example, since there's a struct Brush defined in the current namespace. struct Rect refers to the global struct definition, though, since the MyDll namespace doesn't have its own separate definition of struct Rect.

In some cases, you might want to override the implied local namespace reference to refer to a global name. Suppose, for example, that you also had a global struct Color, in addition to the one defined in the MyDll namespace as in the examples. You could explicitly refer to the global name by using the "::" prefix with no namespace.

dll.define(` struct Color { ... }; // defines a global struct ::Color namespace MyDll { struct Color { ... }; // defines struct MyDll::Color void FillRect2(struct Rect *r, struct ::Color *c); // uses the global struct }; `);

Namespaces apply to struct, union, interface, enum, and typedef names. They don't apply to the members of any of those objects, since the members are already scoped to the enclosing types. Namespaces also don't apply to function names, since those don't go into the internal name table.

Predefined type names

The DLL importer's C parser knows about all of the standard C types (int, long, float, etc) and the standard Windows SDK aliases for the basic types (INT, UINT, DWORD, etc). It also pre-defines nearly all of the Windows HANDLE subtypes (HWND, HDC, HBITMAP, etc) and the common null-terminated string types (LPSTR, LPCTSTR, etc). In normal C code, the "T" string types (LPTSTR, LPCTSTR) can be either ANSI (8-bit characters) or Unicode (16-bit characters), according to compiler options, but in PinballY Javascript, they're always Unicode.

For a full, up-to-the-minute list of the predefined types, refer to the primitiveTypes constant object defined in Scripts\System\CParser.js. Any types that you need but that aren't defined there can always be added in your script code via a call like this:

dllImport.define("typedef int *ptrToInt");

Limitations

Working around limitations

The native call mechanism has limitations in what it can do by way of type conversions, since Javascript was never designed to interface directly to native code. Some Windows APIs or other program APIs might be impossible, or just too cumbersome, to call directly through the Javascript interface. In such cases, you can still call the API functions; you'll just have to do it a little less directly. There are a couple of approaches.

Approach 1: Use ArrayBuffer. If the obstacle to calling a particular API is that the API requires a data structure or format that you can't represent through the DLL importer type conversion mechanisms, you can manually pack or unpack the data structure using Javascript's standard ArrayBuffer type. ArrayBuffer is specifically designed to let you manipulate the bytes in a data structure directly, so you can use it to create or interpret any data format that a callee uses. You can use an ArrayBuffer as a parameter anywhere that a native function expects a native pointer type; the DLL importer passes the callee a direct pointer to the ArrayBuffer's underlying byte array.

Approach 2: Write a custom interface DLL. The DLL importer mechanism's main goal is to let you call Windows APIs directly, but you can just as well use it to call your own custom DLLs. If a Windows API or other API that you're trying to use doesn't mesh well with the DLL importer type scheme, you can create your own C++ or C# DLL that calls the underlying API you're trying to use, and then call your DLL from Javascript. Your DLL can reformat parameters to/from the Windows API in a way that makes it usable through the DLL importer type system. Likewise, if there's any other work that's cumbersome or impossible to do in Javascript, you can offload that part of the work to your custom DLL.

This is similar to what you might have to do to write a "plug-in" for other programs. The big difference is that you don't have to conform to any pre-defined plug-in protocol. You just define whatever particular set of functions you need to carry out the API operations you want to perform. You can still do most of the work in Javascript; the DLL is only needed to do whatever twitchy memory buffer setup or interpretation you need to do that can't be expressed through the DLL importer's type system.