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).
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:
- How to Pause VBA Code (uses the
Sleep
API) - CreateGuid: A Reliable Way to Generate GUIDs in VBA (uses the
CoCreateGuid
API)
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
- Go to Project > References...
- Switch to the Available Packages tab
- Enter "windows" in the search box
- 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) - 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.