Writing C DLLs for use with VB

Author: David Zimmer
Site: http://sandsprite.com

Environment: VB6 + VC6

Writing C DLLs for use with VB

As your VB coding develops, one of the logical steps is to learn how to integrate in C/C++ code with your VB projects.

There are allot of reasons you might want to do this. C/C++ has tons of advanced code examples out there, and intristically has more built in power than VB.

This article is an introduction to writing C Dlls for use with VB. This file is not a definitive resource, I am writing this from what I know at the moment and what I have found through research, experimentation and debugging. There is actually allot of stuff to know...way more than I can describe all inline here. So take this, and the code, as a rough guide to help you on your journey. I do not have the time to detail everything or write up a complete paper on everything you need to know.

This article is half discussion, half tips and tricks. Its aim is to get experienced VB developers with some background in C up and running making standard C dlls for use with VB. I have tried to put together as practical an example as I could and kept it as clear and direct as possible. This information has been put together through the knowledge gained across many books and many test projects.

If you are new to C and maybe even writing API declarations...it will take time to soak it all up, and you will have to search out other resources to help drive in the ideas. Relevant books that I recommend include

  • Johnathan Morrison's C++ for VB Programmers - excellent resource for VB developers looking to code in C
  • Bruce Mckinney's HardCoreVB - on your VB6 MSDN CD in the Books section - old but good
  • Dan AppleMan's Guide to the WinAPi - good appendix on how VB calls DLLs
  • Matt Curland's PowerVB - Advanced VB topics mostly COM related but also some standard DLL stuff
  • VB4DLL.txt - Comes with VB, describes how VB calls C Dlls
There are allot of really crappy books out there just waiting to soak your pockets dry (Like the entire SAMS Teach yourself series)

I personally recommend all of the books above, and I consider them professional level references that are worthy of any professional developers library. (IE they are worthy references even after you have read them and worth hanging onto as opposed to the textbook type books that you skim once and never open again (like most college textbooks):

The example project download will show you several ways to pass strings to your C DLL, how to use Variants, SafeArrays, standard arrays use call back functions, and even inline asm.

First let me introduce you to some Visual C terminology. There are several ways to create a DLL using Visual C++. In VB, all of the DLLs we create are ActiveX DLLs, which means they are actually COM servers housed in a DLL file.

You can make ActiveX dlls in VC, however we are going to take a more basic approach and write a traditional standard call Dll so we donít have to worry about the overhead and complexity of ATL and complex raw COM types.

In VB we are used to interacting with these traditional Dlls in the form of the Windows APIs. Traditional Dlls require the use of manually entered Declare statements to use them in VB.

To start off, C defines several possible function calling conventions. The calling convention describes in what order arguments are pushed onto the stack and who is responsible for cleaning up the stack when the function returns.

For our C-VB interactions we must use the standard calling convention (__stdcall) because this is what VB expects. If you forget to include this, you will get an error "Bad DLL Calling Convention" in the IDE when you try to run your code.

The next thing we need to know is how the map our commonly used VB type into something we can deal with in C. Because of C's power and flexibility, there are actually multiple ways we can deal with our VB Datatypes. The most basic types are those that have a direct C counter part. The VB Byte type for example, takes up one byte of memory and maps to the C char type. The VB Long takes up 4 bytes of memory and maps to the C int type.

When you look at a API declaration you may have noticed that some of the arguments are passed ByRef and some are passed ByVal. (If the API Declaration does not define either, it is ByRef by default.) So what does this mean to our C function?

Just like with VB functions, a ByRef argument means to pass in the actual variable used in the calling code. This is done by passing in a pointer to the original variable.(A pointer is just the numeric address in memory of the original variable). A ByVal argument type means to push a copy of the variables value onto the stack for the called function to use.

Lets see an example.

vb function Declare : ...(ByRef val1 as long , ByVal val2 as long)
C function Prototype: ...(int *val1, int val2)

It is very important you understand the difference between the two, when you get to be writing DLL Declare statements for your own Dlls, the ByVal, ByRef mixups are going to cost you many crashs until you get the feel for it. When you are trying out a new Declare statement or testing a new DLL it is best to compile your VB Exe first and then run that.

If you got something wrong and are running in the IDE, the entire IDE will crash and you could loose your work. Always set VB to auto save changes every time you run from the IDE to help prevent lost work. Common error messages include "Bad DLL Calling Convention", "Can not locate entry point for.." , and memory read/write exceptions caused by trying to reference memory at some low address. If your code works fine, but then crashs when the function is ready to return back to VB, you could have either used the wrong calling convention (stack not cleaned up) or you could have accidentally declared a return type from the C function but forgot it in the VB Declare (or the reverse of that)

We will get into some general debugging hints latter on.

Next we are going to look at the VB String type. for which we (again) need a little more background.

VB is based around COM and uses all COM types for its variables. The VB string we all know and love is actually a BSTR type to C. A BSTR is a Unicode string with a length prefix. This Length prefix allows the string to contain embedded nulls and makes it very easy to read the length of a string (vb Len() function uses this) .

Standard C coding usually uses char arrays (byte arrays) to hold its strings. These strings are typically held in a fixed size array and terminated with a null character ( Chr(0) ). When we work with strings between C and VB we have 2 options. We can either send the string as our native BSTRs or we can specify to have the VB runtime convert our BSTR to an standard null terminated C string for us as it makes the call. If we pass our String ByRef to the C function, it will be passed as a BSTR. If we pass it ByVal then the VB runtime will make a copy of our string, convert it to a null terminated byte array of characters, then pass that to the called function. If your C function receives the BSTR, you can use the Sys*String functions (examples provided) to read & modify it as you wish. If you are dealing with the byte array C style string, you can read and access it as you would a normal C String. If the byte array is big enough, you can actually modify its contents to be returned to VB. This is because on return the VB runtime copies the byte array back into the BSTR variable we passed into it. You cannot however exceed the bounds of the byte array you were passed or else you will be corrupting other things in memory and will most likly crash. When working with a native BSTR you are free to modify the size of the string through the Sys*String functions (ex. SysReAllocString) because the API will handles its own memory management.

The books mentioned above are good resources for more in depth discussion on this.

The next common area people wonder about is how to pass and access arrays.

There are two common ways to do this. One uses pointer arithmetic and the other uses the standard VB Array Type, which is a SAFEARRAY as C knows it. SAFEARRAY struc (Type as vb developers know it) and is defined in oleauto.h

First we will describe the pointer arithmetic method.

Array Elements are laid out sequentially in memory and take up a known number of bytes based on their type. An array of Longs will be a block of memory where every 4 bytes defines one element.

If we pass the first element of our VB Array byRef, we are actually passing in the address of the beginning of that array. If we also include how many elements are in the array as a function argument, we can now loop through all the sequential memory locations to get the corresponding values.

See the code for an example of this. A Word of caution, pointer arithmetic in C goes by the size of the type of pointer you are using. an Int type will increment 4 bytes of memory for each element accessed, a char one byte, make sure you use the right type of pointer to correspond to the VB type you pass in. Do not work with BSTR strings in this way.

The next way to work with arrays between VB and C is to use the native VB array object, the SAFEARRAY mentioned above. You can directly pass in a SAFEARRAY ** as a function argument for use in C by just including oleauto.h header file which contains its struc definition. See the example code for usage. With this method you do not need to pass in the number of array elements because it is contained in the SAFEARRAY structure.

The next thing you may want to work with is Variant data. The vb Variant type is a handy thing, it can contain data of any type and you can change it on the fly...but how is that data really held behind the scenes and how can we access it in C? The VARIANT data structure is actually a large union with a field defined for each different possible data type. An int descriptor field precedes the union and is used as a flag to mark what type of data the VARIANT union currently contains. To use a VARIANT you receive as an argument, first test its .vt member for variable type, then you can access the specific field in the structure for that type to get the actual variable data.

The last commonly sought after example I have included is that of how to perform a callback to a VB function from your C Dll by using a function pointer you supplied in VB. In C you can define function pointer variables that will let you call a function just by having its address. Vb has no such native support although there are workarounds (kind of) . Anyway when you are setting up to have C use a callback for your VB function there are a couple things worth pointing out.

First make sure to declare your function pointer prototype as __stdcall. Second, In VB your callback function has to be in a module. To get the address of a function in VB, you need to use the AddressOf operator, which only works for functions contained in modules. Why? Because forms and classes are instances that could go away. Module functions are guaranteed to exist and not change for the life of the application, so the VB designers made an attempt to save you from yourself by not letting you use it in other situations. If your adventurous, you may be able to snag a classes VTable pointer to a specific function and pass it into your C DLL for callback on class functions. I havenít tried this but it sounds like fun :) I will leave that as an experiment up to the reader.. If you know what I am talking about then you can figure out how to test it ;)

One other aspect of compiling a C DLL for use with VB I should mention is the use of a .def file. The .def file is a simple format plain text file that lists the exact function names you want used in the export table of your DLL. If you do not include it, the compiler will mangle your function names to include other bits of info. Use the .def file to prevent this.

Remember, every time you add a new function to your C DLL, you are going to have to add its function name to the list of DLL Exports in the .def file, as well as write a new Declare statement to include in your VB app. If you get an error "Entry point cannot be found" when trying to run your VB app, then there is a good chance you forgot to add in the new function name to the def file exports list.

Lastly its worth noting that there are multiple ways to pass stuff. Here are some other alternatives for char pointers:

void test(char* x)
declare sub test1 lib "x.dll" alias "test" (x as byte)
declare sub test2 lib "x.dll" alias "test" (byval x as string)
declare sub test3 lib "x.dll" alias "test" (byval x as long)

dim b(50) as byte
dim s as string

s = space(50)
test1 b(0) 
test2 s
test3 varptr(b(0))

Now onto our

Debugging tips and tricks:

  1. Compile your VB exe with debugging info, Project -> Properties -> Compile tab
  2. Compile your C DLL as a debug release -> Build ->Set Active Configuration
  3. You can call the debugger from VB Code by using DebugBreak API (then press F10 twice to step to code)
If you Compiled your VB ExE with debug info, you will be able to see your VB Source interspersed with the actual Assembly that is running, with the VC other debug tools this is very powerful way to see what is going on!

If you Compiled your DLL in debug release, then you can set breakpoints in your C Code and have it drop you into the debugger when it gets called.

To get to a break point in your DLL compile a copy of your vb project in the same folder as the debug dll then press F5 in the VC IDE. It will ask you which executable you would like to startup for debugging, select the copy you placed next to the debug dll and it will load that instance of the dll which the debugger can break on. When you are at a VC breakpoint like this, you will have very nice debug tools at your fingertips that are way beyond what the VB debugger has.

Learn to use the VC debugger well..if you really want to expand your skills to that next level, it will soon become your new best friend.

If you get errors on startup that you cannot set breakpoints and some have been removed, it is probably because you are compiling in release mode without debug info. You really do not want to try to debug looking only at asm.


Other Integration Examples :