| title | Enhanced API Declarations |
|---|---|
| parent | Advanced Features |
| nav_order | 4 |
| permalink | /Features/Advanced/API-Declarations |
twinBASIC provides several enhancements to API and method declarations to make working with external libraries easier.
The DeclareWide keyword, in place of Declare, disables ANSI<->Unicode conversion for API calls. This applies both directly to arguments, and to String arguments inside a UDT. For example, the following are equivalent in functionality:
Public Declare PtrSafe Sub FooW Lib "some.dll" (ByVal bar As LongPtr)
Public DeclareWide PtrSafe Sub Foo Lib "some.dll" Alias "FooW" (ByVal bar As String)
Both represent a fully Unicode operation, but the allows direct use of the String datatype without requiring the use of StrPtr to prevent conversion.
Warning
This does not change the underlying data types-- the String type is a BSTR, not an LPWSTR, so in the event an API returns a pre-allocated LPWSTR, rather than filling a buffer you have created, it will not provide a valid String type. This would be the case where an API parameter is given as [out] LPWSTR *arg.
An API declaration, a procedure, a delegate or an interface member can name its calling convention. The keyword goes directly after the name. Without one, the convention is stdcall, as in VBA.
| Keyword | Convention | Arguments on win32 | Stack cleaned by |
|---|---|---|---|
| (none) | stdcall | all on the stack | the callee |
| CDecl | cdecl | all on the stack | the caller |
| ThisCall | thiscall | the first in the ECX register, the rest on the stack | the callee |
| FastCall | fastcall | the first two in ECX and EDX, the rest on the stack | the callee |
The four follow the Microsoft C/C++ conventions of the same names: __stdcall, __cdecl, __thiscall and __fastcall. A declaration must name the convention its counterpart actually uses. A mismatch passes the arguments in the wrong places, leaves the stack unbalanced, and usually crashes.
A 64-bit build has only one calling convention. There, all four keywords are accepted and have no effect.
A declaration names at most one convention. Two together are compile error TB5258, Too many calling conventions defined, only one allowed.
An API declaration, here with DeclareWide:
Private DeclareWide PtrSafe Function _wtoi64 CDecl Lib "msvcrt" (ByVal psz As String) As LongLong
A __fastcall export. On a 32-bit build, ntdll exports RtlUlongByteSwap with the fastcall convention:
Private Declare PtrSafe Function RtlUlongByteSwap FastCall Lib "ntdll" (ByVal Source As Long) As Long
A procedure exported from a standard DLL:
[ DllExport ]
Public Function MyExportedFunction CDecl(value1 As Long, value2 As Long) As Long
' ...
End Function
Some C++ interfaces are not COM interfaces, and their methods use thiscall rather than stdcall. RichEdit's ITextServices and ITextHost are examples. Mark each such member ThisCall in the Interface definition:
[InterfaceId("FF7DD8F0-0CCC-4197-86FB-A5591A6BA9B3")]
[OleAutomation(False)]
Interface ICounter Extends IUnknown
Function Add ThisCall(ByVal Amount As Long) As Long
Property Get Total ThisCall() As Long
End Interface
A class that implements the interface must repeat the convention on each member. An implementation without it, or with a convention the interface member does not have, is an error (TB5004, Implemented interface member must use the same calling convention as the interface definition).
Class Counter
Implements ICounter
Private mTotal As Long
Private Function ICounter_Add ThisCall(ByVal Amount As Long) As Long Implements ICounter.Add
mTotal = mTotal + Amount
Return mTotal
End Function
Private Property Get ICounter_Total ThisCall() As Long Implements ICounter.Total
Return mTotal
End Property
End Class
On a 32-bit build, a ThisCall implementation takes this in the ECX register and removes its own arguments from the stack, so native code calls it through the interface's vtable as it calls a C++ virtual method. An error raised in the implementation reaches the native caller as a failure HRESULT, as from any member without [PreserveSig]. Code compiled with LLVM does not pass the error on yet; see Language support.
Note
In BETA 1005, a class cannot implement a member marked [PreserveSig]: the compiler reports the implementation as unmatched. Interfaces of this kind often return a value directly rather than an HRESULT, and are declared with [PreserveSig] for that reason. For a member that returns a 4-byte value, such as a Long or a C BOOL, declare it in the interface the class implements as a Sub without [PreserveSig], and return the value with Err.ReturnHResult. Native code that calls this member as long __thiscall Count() gets 3 as its return value:
[InterfaceId("2C4E8A61-5B3F-4D7E-9C1A-6E0B2F4D8A13")]
[OleAutomation(False)]
Interface IItems Extends IUnknown
Sub Count ThisCall()
End Interface
Class Items
Implements IItems
Private Sub IItems_Count ThisCall() Implements IItems.Count
Err.ReturnHResult = 3
End Sub
End Class
An ActiveX DLL's type library records a ThisCall or FastCall member as stdcall, and a CDecl member as cdecl. [OleAutomation(False)] does not leave the interface out of the type library; it clears the flag that marks the interface as compatible with OLE Automation.
A callback passed to an API must use the convention the API calls it with. Declare a Delegate with that convention, use it as the parameter type, and give the target procedure the same keyword. AddressOf of a procedure with a different convention is accepted with a warning only (TB0026).
Note
In BETA 1005, on a 32-bit build, a call made from twinBASIC code through a ThisCall or FastCall delegate passes its arguments as stdcall does, and fails with Bad DLL definition. Calling the procedure directly works. CDecl delegates are not affected, and neither is a 64-bit build.
The following example performs a quicksort using the C runtime's qsort function, which calls its comparator with the cdecl convention:
Private Delegate Function LongComparator CDecl ( _
ByRef a As Long, _
ByRef b As Long _
) As Long
Private Declare PtrSafe Sub qsort CDecl _
Lib "msvcrt" ( _
ByRef pFirst As Any, _
ByVal lNumber As Long, _
ByVal lSize As Long, _
ByVal pfnComparator As LongComparator _
)
Public Sub CallMe()
Dim z() As Long
Dim i As Long
Dim s As String
ReDim z(10) As Long
For i = 0 To UBound(z)
z(i) = Int(Rnd * 1000)
Next i
qsort z(0), UBound(z) + 1, LenB(z(0)), AddressOf Comparator
For i = 0 To UBound(z)
s = s & CStr(z(i)) & vbNewLine
Next i
MsgBox s
End Sub
Private Function Comparator CDecl( _
ByRef a As Long, _
ByRef b As Long _
) As Long
Comparator = a - b
End Function
Simple UDTs can be passed ByVal in APIs, interfaces, and any other method. In VB6 this requires workarounds such as passing each member separately.
' Context for the two samples below: the Windows types they pass. A reader has
' these from a Windows declarations package such as WinDevLib; they are written
' out here so the declarations can be compiled. They sit at the top of the fence,
' not inside a Module block, so the batcher sees their names and keeps this page
' away from other pages that declare a POINT.
Public Type POINT
x As Long
y As Long
End Type
Public Enum BOOL
CFALSE = 0
CTRUE = 1
End Enum
Public Enum KeyStateMouse
MK_LBUTTON = &H1
MK_RBUTTON = &H2
MK_SHIFT = &H4
MK_CONTROL = &H8
MK_MBUTTON = &H10
End Enum
Public Enum DROPEFFECTS
DROPEFFECT_NONE = 0
DROPEFFECT_COPY = 1
DROPEFFECT_MOVE = 2
DROPEFFECT_LINK = 4
End Enum
' ...and the data-object interface that IDropTarget's methods take.
[InterfaceId("0000010E-0000-0000-C000-000000000046")]
Public Interface IDataObject Extends stdole.IUnknown
Sub GetData()
End Interface
Public Declare PtrSafe Function LBItemFromPt Lib "comctl32" (ByVal hLB As LongPtr, ByVal PXY As POINT, ByVal bAutoScroll As BOOL) As Long
Interface IDropTarget Extends stdole.IUnknown
Sub DragEnter(ByVal pDataObject As IDataObject, ByVal grfKeyState As KeyStateMouse, ByVal pt As POINT, pdwEffect As DROPEFFECTS)
' ...
End Interface
and so on. For this feature, a "simple" UDT is one that does not have members that are reference counted or are otherwise managed in the background, so may not contain interface, String, or Variant types. They may contain other UDTs.
With cdecl calling convention fully supported, twinBASIC can also handle variadic functions. In C/C++, those functions contain an ellipsis ... as part of their arguments. This is represented in tB As {ByRef | ByVal} ParamArray ... As Any(). Note that ByRef or ByVal must be explicitly marked; implicit ByRef is not allowed.
Using the given C/C++ prototype:
int WINAPIV wsprintfW(
/* [out] */ LPWSTR unnamedParam1,
/* [in] */ LPCWSTR unnamedParam2,
/* ... */
);The twinBASIC declaration and function using it can be written as shown:
Private DeclareWide PtrSafe Function wsprintfW CDecl _
Lib "user32" ( _
ByVal buf As String, _
ByVal format As String, _
ByVal ParamArray args As Any() _
) As Long
Private Sub Test()
Dim buf As String = Space(1024)
wsprintfW(buf, "%d %d %d", 1, 2, 3)
MsgBox buf
End Sub
For functions which contain the va_list type as part of their arguments the ParamArray declaration must be ByRef.
The [PreserveSig] attribute was described earlier for COM methods, but it can also be used on API declares. For APIs, the default is True. So therefore, you can specify False to rewrite the last parameter as a return.
' Context for the samples below: the shell interface they return. A reader has
' this from the Windows shell type library; it is written out here so the two
' declarations can be compiled.
[InterfaceId("000214E6-0000-0000-C000-000000000046")]
Public Interface IShellFolder
Sub ParseDisplayName()
End Interface
Public Declare PtrSafe Function SHGetDesktopFolder Lib "shell32" (ppshf As IShellFolder) As Long
can be rewritten as:
[PreserveSig(False)]
Public Declare PtrSafe Function SHGetDesktopFolder Lib "shell32" () As IShellFolder