WinDevLib: A Better Way to Call Windows API Functions in twinBASIC

Stop struggling to declare your Windows API calls in twinBASIC. Use the Windows Development Library twinPACK and let fafalone do all the hard work for you.

WinDevLib: A Better Way to Call Windows API Functions in twinBASIC

While best known for its beginner-friendly features like plain English syntax and ahead-of-its-time memory safety, classic Visual Basic's ability to directly call C++ Windows API functions via the Declare keyword also makes it a deceptively powerful language.

Unfortunately, crafting proper Declare statements that don't lead to hard crashes at runtime requires advanced knowledge of both C++ and VB.  That means most of us mere mortals hand-retype our Declare statements from sample code in dead-tree books or copy and paste examples from venerable late '90s/early '00s websites like AllAPI.net, The Access Web, or Lebans.com.

The good news with twinBASIC is that all of the working VB6 and VBA API code you've accumulated over the years will continue to work without requiring any changes.

The even better news?  You probably won't need to write another API declare statement ever again.

The Windows Development Library

The Windows Development Library (aka, WinDevLib) is a twinBASIC package from VB6 and twinBASIC guru fafalone (aka, Jon Johnson).  

GitHub - fafalone/WinDevLib: Windows Development Library for twinBASIC
Windows Development Library for twinBASIC. Contribute to fafalone/WinDevLib development by creating an account on GitHub.

This twinPACK provides standard declare statements for an ever-expanding list of common Windows libraries, as described in the project's GitHub readme (emphasis mine):

In addition to the 2200+ common COM interfaces, WinDevLib now includes expansive coverage of Windows APIs from all the common modules. This makes it similar to working in C++ with #include <Windows.h> and a few others. Currently, approximately 5,500 of the most common APIs have been added- redone by hand from the original headers, in order to restore 64bit type info lost in VB6 versions, avoid the errors of automated conversion tools (e.g. Win32API_PtrSafe.txt is riddled with errors), and make them friendlier by converting groups of constants associated with a variable into an Enum so it comes up in Intellisense. This takes advantage of tB's ability to provide Intellisense for types besides Long in API defs (hopefully UDTs soon, this project has provisioning for that).

So, what does this look like in practice?

Using WinDevLib: Step-by-Step

Step 1.  Open twinBASIC and choose New Console App

Step 2. Use Sample Code with Common API Declares

The sample code below includes simple API code from a couple of articles I've published in the past:

Remove all the code in the MainModule.twin file and replace it with the code below:

Module MainModule
    'see: https://nolongerset.com/how-to-pause-vba-code/
    #If VBA7 Then
        Public Declare PtrSafe Sub Sleep Lib "kernel32" (ByVal dwMilliseconds As Long)
    #Else
        Public Declare Sub Sleep Lib "kernel32" (ByVal dwMilliseconds As Long)
    #End If
    
    #If VBA7 Then
        Private Declare PtrSafe Function CoCreateGuid Lib "ole32" (id As Any) As Long
    #Else
        Private Declare Function CoCreateGuid Lib "ole32" (id As Any) As Long
    #End If
    Public Sub Main()
        Console.Cls
        Dim i As Long
        For i = 1 To 5
            Console.WriteLine(CreateGUID())
            Sleep 500
        Next
    End Sub

    ' ----------------------------------------------------------------
    ' Procedure  : CreateGUID
    ' Author     : Dan (webmaster@1stchoiceav.com)
    ' Source     : http://allapi.mentalis.org/apilist/CDB74B0DFA5C75B7C6AFE60D3295A96F.html
    ' Adapted by : Mike Wolfe
    ' Republished: https://nolongerset.com/createguid/
    ' Date       : 8/5/2022
    ' ----------------------------------------------------------------
    Public Function CreateGUID() As String
        Const S_OK As Long = 0
        Dim id(0 To 15) As Byte
        Dim Cnt As Long, GUID As String
        If CoCreateGuid(id(0)) = S_OK Then
            For Cnt = 0 To 15
                CreateGUID = CreateGUID & IIf(id(Cnt) < 16, "0", "") + Hex$(id(Cnt))
            Next Cnt
            CreateGUID = Left$(CreateGUID, 8) & "-" & _
                         Mid$(CreateGUID, 9, 4) & "-" & _
                         Mid$(CreateGUID, 13, 4) & "-" & _
                         Mid$(CreateGUID, 17, 4) & "-" & _
                         Right$(CreateGUID, 12)
        Else
            MsgBox "Error while creating GUID!"
        End If
    End Function
    
End Module

Press [F5] to execute the code.  You should get a console window that looks like this:

Step 3. Add the Windows Development Library twinPACK Package

  1. Go to Project > References...
  2. Switch to the Available Packages tab
  3. Enter "windows" in the search box
  4. Check the box next to "Windows Development Library for twinBASIC"
    (NOTE: upon checking the box, the package will immediately begin to download in the background; when the download finishes, the package name will have "[IMPORTED]" prepended to its name)
  5. Click [Save Changes]

Step 4. Restart the Compiler

Click the "Restart compiler" button next to the build type selector to restart the twinBASIC compiler.  This may not be necessary in future versions of twinBASIC, but as of BETA 504 it seems to be the most reliable way to ensure the newly referenced package is recognized by the IDE:

Step 5. Remove the Declare Statements from the Code

Delete the Declare ... Sleep statements from the top of the code module.  Here's what the code should look like after removing the statements:

Module MainModule

    #If VBA7 Then
        Private Declare PtrSafe Function CoCreateGuid Lib "ole32" (id As Any) As Long
    #Else
        Private Declare Function CoCreateGuid Lib "ole32" (id As Any) As Long
    #End If
    Public Sub Main()
        Console.Cls
        Dim i As Long
        For i = 1 To 5
            Console.WriteLine(CreateGUID())
            Sleep 500
        Next
    End Sub

    ' ----------------------------------------------------------------
    ' Procedure  : CreateGUID
    ' Author     : Dan (webmaster@1stchoiceav.com)
    ' Source     : http://allapi.mentalis.org/apilist/CDB74B0DFA5C75B7C6AFE60D3295A96F.html
    ' Adapted by : Mike Wolfe
    ' Republished: https://nolongerset.com/createguid/
    ' Date       : 8/5/2022
    ' ----------------------------------------------------------------
    Public Function CreateGUID() As String
        Const S_OK As Long = 0
        Dim id(0 To 15) As Byte
        Dim Cnt As Long, GUID As String
        If CoCreateGuid(id(0)) = S_OK Then
            For Cnt = 0 To 15
                CreateGUID = CreateGUID & IIf(id(Cnt) < 16, "0", "") + Hex$(id(Cnt))
            Next Cnt
            CreateGUID = Left$(CreateGUID, 8) & "-" & _
                         Mid$(CreateGUID, 9, 4) & "-" & _
                         Mid$(CreateGUID, 13, 4) & "-" & _
                         Mid$(CreateGUID, 17, 4) & "-" & _
                         Right$(CreateGUID, 12)
        Else
            MsgBox "Error while creating GUID!"
        End If
    End Function
    
End Module

Press [F5] to execute the code.  It should work just like before.

Now, right-click on the call to Sleep in the Main subroutine and choose "Go To Definition."  The code immediately takes you to where the Sleep routine is defined in the twinPACK WinDevLib source code:

/Packages/WinDevLib/Sources/wdAPI.twin

Step 6. Update Code to Handle More Explicit Types

In our original code, we define the CoCreateGUID function with the following signature:

Private Declare PtrSafe Function CoCreateGuid Lib "ole32" (id As Any)

Notably, the id variable is defined As Any.

If we check out the CoCreateGuid function in WinDevLib, the signature is slightly different:

Public Declare PtrSafe Function CoCreateGuid Lib "ole32" (ByRef pGUID As UUID) As Long

Here we see both a specific return type for the function (an explicit As Long as opposed to the implicit As Variant), and also a UUID in place of Any in the function parameter.

Luckily, we don't have to figure out what a UUID type looks like.  We can just look it up in WinDevLib:

In the original code, we declared the id variable as a 16-byte data structure:

Dim id(0 To 15) As Byte

At the binary level, the 16-byte array is identical to the UUID type, which also occupies a total of 16 bytes:

Data1 As Long          '4 bytes
Data2 As Integer       '2 bytes
Data3 As Integer       '2 bytes
Data4(0 To 7) As Byte  '8 bytes
                       --------
                       16 bytes

This also required us to make some changes to the CreateGUID function.  Here's the updated code using the more explicit UUID type rather than As Any:

    Public Function CreateGUID() As String
        Dim id As UUID
        If CoCreateGuid(id) = S_OK Then
            CreateGUID = Hex(id.Data1) & "-" & _
                         Hex(id.Data2) & "-" & _
                         Hex(id.Data3) & "-"
            Dim i As Long
            For i = 0 To 1
                CreateGUID = CreateGUID & _
                             Hex(id.Data4(i))
            Next i
            CreateGUID = CreateGUID & "-"
            For i = 2 To 7
                CreateGUID = CreateGUID & _
                             Hex(id.Data4(i))
            Next

        Else
            MsgBox "Error while creating GUID!"
        End If
    End Function

Step 7.  Verify Strings are Handled Correctly

This particular step was not an issue for us, but the WinDevLib manual discusses the interplay between twinBASIC code and API calls and how that interplay varies from what we are used to with VB6/VBA.

Here's an excerpt from the WinDevLib project readme (emphasis in original):


WinDevLib API standards

This was mentioned above, but it's worth going into more detail. In addition to the COM interfaces, WinDevLib has a large selection of common Windows APIs; this is a much larger set than oleexp. WinDevLib and twinBASIC represented the best opportunity there would be to modernize standards... most VB programs are written with ANSI versions of APIs being the default. This is not the case with WinDevLib. With very few exceptions, APIs are Unicode by default-- i.e. they use the W, rather than A, version of APIs e.g. DeleteFile maps to DeleteFileW rather than DeleteFileA. The A and W variants use String/LongPtr, and in almost all cases, the mapped version uses String with twinBASIC's DeclareWide keyword-- this disables Unicode-ANSI conversion, so you can still use String without StrPtr or any Unicode <-> ANSI conversion. Note this usually only applies to strings passed as input, APIs passing a LPWSTR that's allocated externally will still be LongPtr, as they're not in the same BSTR format as VBx/TB strings.

All APIs are provided, as a minimum, as the explicit W variant, and an untagged version that maps to the W version. Some, but not all, APIs also have an explicit A variant defined that will perform the normal ANSI conversion for compatibility purposes. This is decided on a case by case basis depending on my impression of how much legacy code is around that needs the ANSI version. All new code should use the Unicode versions.

UDTs used by these calls are also supplied in the same manner, the W variant, an untagged variant that's the same as the W version, and in some cases, an A version. UDTs always use LongPtr for strings, even the untagged versions for DeclareWide.

If you have any doubts about which API is being called, twinBASIC will show the full declaration when you hover your cursor over the API in your code.


If It Ain't Broke, Don't Fix It

If steps 6 and 7 above scared you, don't let it worry you.

Remember, you can still use the Declare keyword to override the WinDevLib declaration for any API function or subroutine.  As we showed in step 2 above, copying and pasting VBA code into a twinBASIC project Just Works™, even if you added the Windows Development Library as a project reference.  If you have solid code that's been running on VBA for years, leaving the existing Declare lines intact is probably the safest way to port it.

That said, any new development should attempt to use the stricter types as defined in WinDevLib, whenever possible.

All original code samples by Mike Wolfe are licensed under CC BY 4.0