Visual Basic DLL

TODO: Convert wiki syntax

Creating a MinGW DLL for Use with Visual Basic

Below are two examples of techniques which facilitate the use of use of MinGW to create DLLs, exporting functions which may be called from Visual Basic. The first of these uses C or C++ as the implementation language; the second uses GNAT/GCC.


Example 1: A DLL for Visual Basic, Programmed in C or C++

NOTE: This description is only for Visual Basic 6, not DotNet.

  • Step 1: Create your DLL.

VB can only call __stdcall functions, not __cdecl functions. So we must export all our functions as __stdcall. Create a DLL with the following code template:

extern "C" // only required if using g++
{

__declspec (dllexport) void __stdcall FooBar (void)
{
  return;
}

__declspec (dllexport) DWORD __stdcall FooBarRet (DWORD MyValue)
{
  return MyValue;
}

__declspec (dllexport) BOOL __stdcall DllMain(HANDLE hModule, DWORD  ul_reason_for_call, LPVOID lpReserved)
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
        break;
    case DLL_THREAD_ATTACH:
        break;
    case DLL_THREAD_DETACH:
        break;
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

} // extern "C"

When compiling, add "--add-stdcall-alias" to your linker flags. If you're passing linker flags via g++, it should be "-Wl,--add-stdcall-alias". This adds an undecorated alias for the exported function names that is simply the name of the function. By default, the functions are exported with an @nn appended to the function name.

The DllMain function is called by the system when your dll is loaded and unloaded from a process, as well as when threads attach and detach (I'm not sure what that means, but it's in the case statement). If you have anything important to do in your dll when these events occur, this is the place to do it. Initializing WinSock is a popular thing to do here so that the WinSock function calls don't all return 10093 -- WSANOTINITIALISED. If you don't export this function yourself, it won't be automatically exported as in Visual Studio.

  • Step 2: Call DLL from VB

Under VB, create the library call such as:

Private Declare Sub ~FooBar Lib "mydll" ()
Private Declare Sub ~FooBarRet Lib "mydll" (~ByVal ~MyValue As Long) As Long

That's all. As an alternative, you can create a ~TypeLib, but that's another story.

  • Variables
_*w32api type*_|
  _*VB type*_|
    _*Automation type*_
BOOL|
  Long|
    BOOL
unsigned long|
  Long|
    Long
LPSTR|
  String <sup>(1)</sup>|
    LPSTR
short int|
  Boolean|
    VARIANT_BOOL
  • (1) : Internally (under VB), a string is a BSTR: a pointer to an array of WCHAR with a 32 bit prefix that declares the number of elements of the array. When calling an API, VB passes a LPSTR (converted from the WCHAR array). A WCHAR array is not a LPWSTR since it can contain embedded NULL WCHARs.

Example 2: Programming a GNAT/GCC + MinGW DLL for Visual Basic

This is an extension of Roger Pearse's The Noddy Guide to using ADA code with Visual Basic. Roger effectively gives up on the possibility of a Windows DLL under GNAT/GCC. Windows has improved somewhat since his original article. So has gcc. The following outlines my attempt.

The spec, API.ADS:

  • function Factorial, which accepts a Win32.LONG and returns a Win32.LONG (I gave up on C.int as it was too small for a Factorial of 16)
  • function GetCount, which returns the value of the global Count (another Win32.LONG)
  • procedure Mangle, which accepts a Win32.LPWSTR and attempts to modify it _in situ_
  • function Pattern, which returns a long pointer to some string data
  • procedures Initialize_API, and Finalize_API, which I understand are mandatory. (If someone knows better, please tell me.)
with Interfaces.C; use Interfaces;
with Win32; use Win32;
package API is
   Count : Win32.LONG := 0;
   function Factorial (Val : Win32.LONG) return Win32.LONG;
   function GetCount return Win32.LONG;
   procedure Mangle( Ptr : Win32.LPWSTR );
   function Pattern return Win32.LPWSTR;

   procedure Initialize_API;
   procedure Finalize_API;

private
   pragma Export (DLL, Initialize_API);
   pragma Export (DLL, Finalize_API);
   --pragma Export (StdCall, Count);
   pragma Export (DLL, GetCount );
   pragma Export (DLL, Factorial );
   pragma Export (DLL, Mangle );
   pragma Export (DLL, Pattern );
end API;

the base, API.ADB:

  • I'm still working through the issue of what to do with Mangle. Perhaps two routines are needed. The first to ask the DLL how much space is needed, with VB the allocating it, and the second then actually poking the data into the allocated space.
  • Pattern returns a pointer to the string data. I rediscovered the technique for reading it out and that's in the VB code below. It also seems that, for as long as the DLL is loaded, the data stored in foo is available for transfer.
with Interfaces.C;
use Interfaces.C;
with Win32; use Win32;
with Win32.Winmain;
with Win32.Winbase;
with System;

package body API is
   function Factorial (Val : Win32.LONG) return Win32.LONG is
      Fact : Win32.LONG := 1;
   begin
      Count := Count + 1;
      for K in 1 .. Val loop
         Fact := Fact * K;
      end loop;
      return Fact;
   end Factorial;

   function GetCount return Win32.LONG is
   begin
      return Count;
   end GetCount;

   procedure Mangle( Ptr : Win32.LPWSTR ) is
      lp : Win32.lpwstr;
      foo : constant Win32.WCHAR_Array := "foo man chew" & Win32.Wide_Nul;
   begin
      lp := Win32.Winbase.lstrcpyW( Ptr, Win32.Addr(foo) );
   end Mangle;

   function Pattern return Win32.LPWSTR is
       foo : constant Win32.WCHAR_Array := "martinet mongoose manticore" & Win32.Wide_Nul;
   begin
      return Win32.Addr(foo);
   end Pattern;

   procedure Initialize_API is
      procedure Adainit;
      pragma Import (C, Adainit);
   begin
      Adainit;
   end Initialize_API;

   procedure Finalize_API is
      procedure Adafinal;
      pragma Import (C, Adafinal);
   begin
      Adafinal;
   end Finalize_API;
end API;

api.def lists the symbols exported, and is needed by gnatdll.

EXPORTS
    factorial
    mangle
    initialize_api
    finalize_api
    pattern
    getcount

This was compiled in Windows XP's CMD. The batch file, build.bat, is as follows. It is assumed that the

debug

and

release

subdirectories already exist:

@echo off
set target=%1
if %1@==@ set target=debug
if not %1@==debug@ if not %1@==release@ set target=debug

echo Building %target% version of dll
gnat make -Pbuild -XSTYLE=%target%
gnatbind -n -x %target%\api.ali -IC:\MinGW\lib\gcc\mingw32\4.2.1\stdarg -IC:\MinGW\lib\gcc\mingw32\4.2.1\win32ada
gnatlink -g -mdll -s -o %target%\api.dll %target%\api.ali -Xlinker --base-file=%target%\api.base
dlltool --dllname api.dll --base-file %target%\api.base --output-exp %target%\api.exp --def api.def
gnatlink -g -mdll -s -o %target%\api.dll %target%\api.ali -Xlinker --base-file=%target%\api.base %target%\api.exp

if %target%==release upx -9 %target%\api.dll

The project file used by gnat make is as follows:

project Build is
 for Source_Dirs use ( "./", "c:\mingw\lib\gcc\mingw32\4.2.1\**" );
 for Object_Dir use "debug";
 for Exec_Dir use "debug";
 for Main use ("api");

 type Style_Type is ("debug", "release");
 Style : Style_Type := external ("STYLE", "debug");

 case Style is
  when "debug" =>
   for Object_Dir use "debug";

  when "release" =>
   for Object_Dir use "release";
   for Exec_Dir use ".";
 end case;

 package Builder is

  case Style is
   when "debug" =>
    for Default_Switches ("Ada")
    use ("-g");

   when others =>
    null;
  end case;

 end Builder;

 package Compiler is

  case Style is
   when "debug" =>
    for Default_Switches ("Ada")
     use ( "-s",
     "-gnata",
     "-gnato",
     "-gnatE");

   when "release" =>
    for Default_Switches ("Ada")
     use ("-O2", "-fno-strict-aliasing");
  end case;

 end Compiler;

end Build;

Now the VB code. Take note of a few things:

  • Exporting as DLL generates decorated names, but dlltool was able to resolve the decorated ones with the undecorated ones specified in api.def
  • For reasons I remember reading somewhere, initialize_api and finalize_api are mandatory, and must be the first and last calls to the DLL
  • Seeing as _woof woof woof woof_ is longer than _foo man chew_,
    s

    ends up containing _foo man chew f woof_

  • As mentioned before, I rediscovered some code for getting data from a pointer and put that in. It's the PointerToString function at the bottom.
Declare Sub initialize_api Lib "api.dll" ()
Declare Function factorial Lib "api.dll" (ByVal x As Long) As Long
Declare Function getcount Lib "api.dll" () As Long
Declare Function pattern Lib "api.dll" () As Long
Declare Sub mangle Lib "api.dll" (ByVal x As Long)
Declare Sub finalize_api Lib "api.dll" ()

' Declaration of the function which copies the character string
Private Declare Function lstrcpy Lib "Kernel32.Dll " Alias "lstrcpyW" (LpszString1 As Any, LpszString2 As Any) As Long

' Declaration of the function which returns the length in the character string
Private Declare Function lstrlen Lib "Kernel32.Dll " Alias "lstrlenW" (ByVal lpszString As Long) As Long

Sub main()
    Dim s As String
    s = "woof woof woof woof"
    initialize_api
    Dim i As Long
    For i = 1 To 16
    Debug.Print factorial(i);
    Next
    Debug.Print
    mangle StrPtr(s)
    Debug.Print Len(s), s
    Dim ptr As Long
    ptr = pattern()
    Debug.Print PointerToString(ptr)
    finalize_api
End Sub

' Converting the pointer to the character string
Function PointerToString(lngPointer As Long) As String
    Dim bytBuffer(255) As Byte
    ' Copying the character string which the pointer points to byte array
    lstrcpy bytBuffer(0), ByVal lngPointer
    ' Cutoff after the null character
    PointerToString = Left(bytBuffer, lstrlen(lngPointer))
End Function

Finally, a sample run under VB6

 1  2  6  24  120  720  5040  40320  362880  3628800  39916800  479001600  1932053504  1278945280  2004310016  2004189184
 19           foo man chew f woof
martinet mongoose manticore

Site Status

Site maintenance completed May 25th, 2012 at 12:38 UTC