I’ve been playing recently with the amBX ambient effects library and devices.
Essentially, it’s an effects platform that allows you to easily create light, wind and rumble effects to coordinate with what’s going on on-screen, in a game, in media players, or what-have-you. The lighting effects are nothing short of fantastic. The rumble and fan effects…. meh. They’re interesting, but I’m not sure where that’ll go.
Regardless, the API for amBX is all C style objects, which means essentially an array of function pointers to the methods of the object; not really an API that VB.net likes to consume. Be sure to grab the amBX developer kit here. You’ll also need to core amBX software available from the main website. Finally, play around with the “Virtual amBX Test Tool”. It’ll allow you to experiment with all the amBX features and functions without having to buy anything at all. Of course, eventually you’ll want to get at least the starter kit, with the lights. But it’s not necessary to begin experimenting.
Hats off to Philips for making this possible!
Here’s an example of the structure definition from the amBX.h header file that comes with the API (I’ve clipped out comments and other bits):
struct IamBX {
amBX_RESULT (*release) (struct IamBX * pThis);
amBX_RESULT (*createLight) (struct IamBX * pThis,
amBX_u32 loc,
amBX_u32 height,
struct IamBX_Light** ppLight);
....
As you can see, each element in the structure is made up of a pointer to a function. The various functions all take, as their first argument, a pointer back to this structure. Under the covers, this is how virtually all object oriented languages function, it’s just the the compiler usually hides all this nasty plumbing so you don’t have to deal with it regularly.
But this presents a problem. VB.net is “managed” code, and as such, it likes things to be wrapped up in nice “managed” bits. There are plenty of good reasons for this, but these function pointers are decidedly not nice and tidy managed bits! So, what to do?
One solution is the approach Robert Grazz took here. It’s a solid solution, no doubt, and I learned a lot from looking at his approach, but, this being VBFengShui, I wanted that same functionality in native VB.net with no additional dll files hanging around!
Delegates to the Rescue!
A delegate in .net is essentially a managed function pointer. In VB.net, they’re most commonly used to handle events, but you wouldn’t really know it, because VB’s compiler does a good job of hiding all that plumbing from you. However, unlike VB6 and earlier, in VB.net, all the plumbing can, if you’re willing, be “brought into the daylight” and used however you want.
There are many, many discussions about delegates for VB.net out on the web. A really good Introduction to the concepts by Malli_S is on codeproject, so I won’t rehash the basics here.
The problem is, delegates in VB.net tend to be generated by using the AddressOf operator, and I already had the function addresses (they’re in that C structure). I just needed to get a delegate that would allow me to call it.
Some Googling turned up several good posts that provided pointers, including this one on StackOverflow. But it wasn’t exactly all the steps necessary.
Getting the C Structure
The first step toward getting something working with amBX is to retrieve the main amBX object. You do this through a standard Windows DLL call.
<DllImport("ambxrt.dll", ExactSpelling:=True, CharSet:=CharSet.Auto)> _
Public Shared Function amBXCreateInterface(ByRef IamBXPtr As IntPtr, ByVal Major As UInt32, ByVal Minor As UInt32, ByVal AppName As String, ByVal AppVer As String, ByVal Memptr As Integer, ByVal UsingThreads As Boolean) As Integer
End Function
Assuming you have the ambxrt.dll file somewhere on your path or in the current dir, the call will succeed, but what exactly does that mean?
Here’s an example of a call to it in C:
if (amBXCreateInterface(
&pEngineHandle,
majorVersion, minorVersion,
(amBX_char*)cAppName.ToPointer(), (amBX_char*)cAppName.ToPointer(),
nullptr, false)
!= amBX_OK)
Essentially, you pass in the Major and minor version numbers of the Version of amBX you need, and two strings indicating the name of your app and the version of your app. No problems with any of that. But that &EngineHandle is the trick.
It means that this call will return a 4 byte pointer to a block of memory that contains the amBX object structure, which is, itself, an array of 4 byte function pointers to the various methods of the amBX object. Therefore, in VB.net, that argument is declared ByRef IamBXPtr as IntPtr.
Now, we need a place to store that “structure of pointers.” Going through the ambx.h file, I ended up with a structure that looks like this:
<StructLayout(LayoutKind.Sequential)> _
Private Structure IamBX
Public ReleasePtr As IntPtr
Public CreateLightPtr As IntPtr
Public CreateFanPtr As IntPtr
Public CreateRumblePtr As IntPtr
Public CreateMoviePtr As IntPtr
Public CreateEventPtr As IntPtr
Public SetAllEnabledPtr As IntPtr
Public UpdatePtr As IntPtr
Public GetVersionInfoPtr As IntPtr
Public RunThreadPtr As IntPtr
Public StopThreadPtr As IntPtr
End Structure
That StructLayout attribute is particularly important. It tells the compiler that the structure should be layed out in memory JUST AS it’s declared in source code. Otherwise, the compiler could possibly rearrange elements on us. With Managed Code, that’d be no problem, but when working with unmanaged C functions, that would not be a good thing!
And finally, we need a way to retrieve that function pointer array and move it into the structure above, so that it’s easier to work with from VB.net. That’s where the System.Runtime.InteropServices.Marshal.PtrToStructure function comes in. This routine will copy a block of unmanaged memory directly into a managed structure.
Private _IamBX As IamBX
Private _IamBXPtr As IntPtr
...
amBXCreateInterface(_IamBXPtr, 1, 0, “MyAppName”, “1.0”, 0, False)
_IamBX = Marshal.PtrToStructure(_IamBXPtr, GetType(IamBX))
And presto, you should now have the _IamBX structure filled in with the function pointers of all the methods of this C “Object”.
Calling the C Function Pointer
At this point, we’ve got everything necessary to call the function pointer. Take the CreateLight function. The C prototype for this function is shown at the top of this page. As arguments, it takes a pointer back to the amBX structure, 2 32bit integers describing the location and height of the light source, and it returns are pointer to another structure, in this case, an amBX_Light structure, which contains another set of function pointers, just like the amBX structure described above.
First, we need to declare a delegate that matches the calling signature of the CreateLight C function we’ll be calling:
<UnmanagedFunctionPointer(CallingConvention.Cdecl)> _
Private Delegate Function CreateLightDelegate(
ByVal IamBXPtr As IntPtr, _
ByVal Location As Locations, _
ByVal Height As Heights, _
ByRef IamBXLightPtr As IntPtr) As amBX_RESULT
The key points here are:
- Make sure you have the CallingConvention attribute set right. Most C libraries will be CDECL, but some libraries are STDCALL.
- Make sure your parameter types match, especially in their sizes. Not doing this will lead to corrupted called or system crashes.
Now, create a variable for the delegate and use the Marshal.GetDelegateFromFunctionPointer function to create a new instance of the delegate, based on the applicable function pointer. Since the function pointer for the CreateLight function is stored in the CreateLightPtr field of the _IamBX structure, we pass that in as the pointer argument.
IMPORTANT NOTE: There are 2 overloads for the Marshal.GetDelegateFromFunctionPointer function. Be sure to use the one what requires a second argument of the TYPE of the delegate to create. Using the other one will fail at runtime because it won’t be able to dynamically determine the type of delegate to create.
Next, call the function via the delegate, just as if it was a function itself.
Finally, use Marshal.PtrToStructure again to copy the array of function pointers returned into another structure, this one formatted to contain the function pointers of the Light object that just got created.
Dim d As CreateLightDelegate = Marshal.GetDelegateForFunctionPointer(_IamBX.CreateLightPtr, GetType(CreateLightDelegate))
Dim r = d(_IamBXPtr, Location, Height, _IamBXLightPtr)
_IamBXLight = Marshal.PtrToStructure(_IamBXLightPtr, GetType(IamBX_Light))
The amBX library is much, much larger than just this little bit I’ve shown here. Once I get the entire thing coded up, I’ll be presenting it here as well.
But in the meantime, if you have a need to interface with C style function pointer objects, don’t let anyone tell you it can’t be done in VB.net
Final Note
As you may or may not have guessed, amBX is a 32bit library and its interfaces, functions and arguments all live in 32bit land. If you’re working with VS2008, BE SURE to set your project to the x86 platform, and NOT “Any CPU” or “x64”! The default, for whatever reason is “Any CPU”, which means things will work properly when run under 32bit windows, but things won’t work well at all if run under a 64bit OS.