Delegate Types for Indirect Invocation
Native support for calling functions through pointers, via the Delegate syntax. A delegate in twinBASIC is a function pointer type compatible with LongPtr. AddressOf returns a delegate type, which is also backward-compatible with LongPtr.
Basic Usage
Defining a Delegate Type
Use the Delegate keyword to define a function pointer type, declaring its parameter signature and return type:
' Define a delegate with no return value (like a Sub)
Public Delegate Sub NotifyHandler(ByVal message As String)
' Define a delegate with a return value (like a Function)
Public Delegate Function MathOp(ByVal A As Long, ByVal B As Long) As Long
' Define a delegate with pointer parameters (for Win32 API callbacks)
Public Delegate Function WndProc(ByVal hwnd As LongPtr, ByVal uMsg As Long, ByVal wParam As LongPtr, ByVal lParam As LongPtr) As LongPtrNote: twinBASIC delegates do not support generic parameters — you cannot write
Delegate Sub Action(Of T)(ByVal item As T). You need to define a concrete delegate type for each parameter type.
Assignment and Invocation
Use AddressOf to obtain a function address and assign it to a delegate variable, then call the delegate directly using function call syntax:
Private Delegate Function Delegate1(ByVal A As Long, ByVal B As Long) As Long
Private Sub Command1_Click()
Dim myDelegate As Delegate1 = AddressOf Addition
MsgBox "Answer: " & myDelegate(5, 6)
End Sub
Public Function Addition(ByVal A As Long, ByVal B As Long) As Long
Return A + B
End FunctionA delegate variable can also be declared first and assigned later — it does not need to be initialized at declaration:
Dim myDelegate As Delegate1 ' Declare first
myDelegate = AddressOf Addition ' Assign later
MsgBox "Answer: " & myDelegate(5, 6)Relationship Between Delegates and LongPtr
twinBASIC delegate types are fully compatible with LongPtr, which means:
- Delegates can be implicitly converted to
LongPtr LongPtrcan be explicitly converted back to a delegate type viaCType- The old VB6 practice of storing callback pointers as
LongPtrstill works
Why Convert to LongPtr?
In class modules, delegate types have some limitations that make storing delegate variables directly not always feasible:
| Operation | Delegate Type | LongPtr |
|---|---|---|
| As a private class member | ❌ Not supported | ✅ Supported |
Assignment with Set | ❌ Not an object | N/A |
Check with IsNot Nothing | ❌ Not supported | ✅ Can compare with 0 |
Assignment with Property Let | ✅ Supported | ✅ Supported |
Assignment with Property Set | ❌ Not supported | N/A |
Therefore, in real-world projects, it is recommended to use LongPtr to store delegate pointers, and convert back to delegate types when invocation is needed.
LongPtr Storage and CType Conversion Pattern
This is a battle-tested complete pattern:
' --- Define delegate ---
Public Delegate Sub ResizeHandler(ByVal newWidth As Long, ByVal newHeight As Long)
' --- Store delegate pointer as LongPtr in a class ---
Public Class MyWindow
' Store function pointer as LongPtr
Private m_OnResize As LongPtr
' Use a boolean flag to track whether it has been set
Private m_HasResizeHandler As Boolean
' Use Property Let (not Set) to set the delegate
Public Property Let OnResize(ByVal handler As ResizeHandler)
m_OnResize = CLngPtr(handler) ' Delegate to LongPtr
m_HasResizeHandler = (handler <> 0) ' Check if not null
End Property
' When triggering the event, convert back to delegate type and call
Public Sub RaiseResize(ByVal w As Long, ByVal h As Long)
If m_HasResizeHandler AndAlso m_OnResize <> 0 Then
Dim h As ResizeHandler = CType(Of ResizeHandler)(m_OnResize)
h(w, h)
End If
End Sub
End ClassSummary of key steps:
- Store:
m_Ptr = CLngPtr(delegateValue)— delegate to LongPtr - Check if null:
delegateValue <> 0or maintain a boolean flag - Invoke:
CType(Of DelegateType)(m_Ptr)— convert LongPtr back to delegate, then call directly
Delegates as UDT Members
Delegate types can be used as members of User-Defined Types (UDT / Structure), replacing traditional LongPtr function pointer fields and providing type safety:
Public Delegate Function CCHookProc(ByVal hwnd As LongPtr, ByVal uMsg As Long, ByVal wParam As LongPtr, ByVal lParam As LongPtr) As LongPtr
Public Type CHOOSECOLOR
lStructSize As Long
hwndOwner As LongPtr
hInstance As LongPtr
rgbResult As Long
lpCustColors As LongPtr
Flags As ChooseColorFlags
lCustData As LongPtr
lpfnHook As CCHookProc ' Delegate function pointer type, not LongPtr
lpTemplateName As LongPtr
End TypeIf you already have code that assigns Long/LongPtr to the lpfnHook member, it will continue to work, but now you also get the type safety advantage of assigning it to a method matching the delegate:
Dim tCC As CHOOSECOLOR
tCC.lpfnHook = AddressOf ChooseColorHookProc
'...
Public Function ChooseColorHookProc(ByVal hwnd As LongPtr, ByVal uMsg As Long, ByVal wParam As LongPtr, ByVal lParam As LongPtr) As LongPtr
End FunctionUsing Delegates in API Declarations
Standard Callback Functions
Delegates can be used directly for callback parameters in API declarations, replacing traditional LongPtr and gaining compile-time type checking:
' Traditional approach (VB6 compatible)
Public Declare PtrSafe Function EnumWindows Lib "user32" ( _
ByVal lpEnumFunc As LongPtr, _
ByVal lParam As LongPtr) As Long
' Delegate approach (recommended, type-safe)
Public Delegate Function EnumWindowsProc(ByVal hwnd As LongPtr, ByVal lParam As LongPtr) As Long
Public Declare PtrSafe Function EnumWindows Lib "user32" ( _
ByVal lpEnumFunc As EnumWindowsProc, _
ByVal lParam As LongPtr) As LongPass the callback function using AddressOf:
Public Sub ListWindows()
EnumWindows AddressOf MyEnumProc, 0
End Sub
Private Function MyEnumProc(ByVal hwnd As LongPtr, ByVal lParam As LongPtr) As Long
' Process each window...
Return 1 ' Continue enumeration
End FunctionCDecl Callbacks
twinBASIC supports CDecl calling convention callbacks. Simply add the CDecl keyword to the delegate definition. The following example uses the C runtime qsort function for quick sorting:
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 SortArray()
Dim z() As Long
Dim i As Long
ReDim z(10) As Long
For i = 0 To UBound(z)
z(i) = Int(Rnd * 1000)
Next i
' AddressOf automatically matches the LongComparator delegate signature
qsort z(0), UBound(z) + 1, LenB(z(0)), AddressOf Comparator
For i = 0 To UBound(z)
Debug.Print CStr(z(i))
Next i
End Sub
Private Function Comparator CDecl( _
ByRef a As Long, _
ByRef b As Long _
) As Long
Comparator = a - b
End FunctionImportant: The calling convention (
CDeclor the defaultStdCall) of the callback function must match the delegate definition, otherwise it will cause stack imbalance and program crashes.
Callbacks in DLL Wrapping
In real-world projects, you often need to wrap DLLs and expose callback interfaces. Here are complete DLL callback wrapping examples.
Scenario: Wrapping an Async Task Library
' === Callback delegate definitions ===
Public Delegate Sub TaskCompleteCallback(ByVal taskId As Long, ByVal result As Long)
Public Delegate Sub TaskProgressCallback(ByVal taskId As Long, ByVal percent As Long)
' === DLL internal class ===
Public Class cAsyncTask
Private m_TaskId As Long
Private m_OnComplete As LongPtr
Private m_OnProgress As LongPtr
Private m_HasComplete As Boolean
Private m_HasProgress As Boolean
Public Sub New(ByVal taskId As Long)
m_TaskId = taskId
End Sub
' Set completion callback — use Property Let, not Set
Public Property Let OnComplete(ByVal cb As TaskCompleteCallback)
m_OnComplete = CLngPtr(cb)
m_HasComplete = (cb <> 0)
End Property
' Set progress callback
Public Property Let OnProgress(ByVal cb As TaskProgressCallback)
m_OnProgress = CLngPtr(cb)
m_HasProgress = (cb <> 0)
End Property
' Simulate async execution
Public Sub Execute()
Dim i As Long
For i = 1 To 100
' Report progress
If m_HasProgress AndAlso m_OnProgress <> 0 Then
Dim pcb As TaskProgressCallback = CType(Of TaskProgressCallback)(m_OnProgress)
pcb(m_TaskId, i)
End If
' ...execute task...
Next i
' Report completion
If m_HasComplete AndAlso m_OnComplete <> 0 Then
Dim ccb As TaskCompleteCallback = CType(Of TaskCompleteCallback)(m_OnComplete)
ccb(m_TaskId, 42)
End If
End Sub
End ClassScenario: Receiving Function Addresses from External Sources
When a DLL's function address is passed in as a parameter (e.g., by the host program), you need to convert the LongPtr to a delegate type before calling it:
' Define callback delegate
Public Delegate Sub ExternalCallback(ByVal eventData As LongPtr)
' DLL exported function: receive external callback address
[DllExport]
Public Function RegisterCallback(ByVal callbackAddr As LongPtr) As Long
' Option 1: Store as LongPtr directly, convert when calling
g_CallbackAddr = callbackAddr
' Option 2: Convert to delegate type immediately
Dim cb As ExternalCallback = CType(Of ExternalCallback)(callbackAddr)
' Now you can call it directly
cb(0) ' Test call
RegisterCallback = 1 ' Success
End Function
' Global storage
Private g_CallbackAddr As LongPtr
' Call the callback when needed
Public Sub FireEvent(ByVal data As LongPtr)
If g_CallbackAddr <> 0 Then
Dim cb As ExternalCallback = CType(Of ExternalCallback)(g_CallbackAddr)
cb(data)
End If
End SubScenario: VB6-Compatible Indirect Assignment
In VB6, AddressOf can only be passed as an argument to a function — it cannot be directly assigned to a variable. While twinBASIC has removed this restriction, if you need to maintain VB6-style compatibility, you can use a helper function to indirectly obtain the address:
' VB6-compatible approach: get AddressOf via helper function
Public Function GetAddressOf(ByVal addr As LongPtr) As LongPtr
GetAddressOf = addr
End Function
' Usage
Dim procAddress As LongPtr
procAddress = GetAddressOf(AddressOf MyCallback)
' Then you can pass procAddress to a DLL or store itThis workaround is not needed in twinBASIC. twinBASIC allows direct assignment using
AddressOf:vbDim procAddress As LongPtr = CLngPtr(AddressOf MyCallback) ' Or assign directly to a delegate variable Dim myCb As ExternalCallback = AddressOf MyCallback
Observer Pattern (Delegate-Based Event Subscription)
Delegates can implement a lightweight observer pattern that is more flexible than WithEvents:
' Define data change delegate
Public Delegate Sub DataChangedHandler(ByVal data As Variant)
' Subject (Observable)
Public Class cDataSource
Private m_Observers As Collection
Private m_Data As Variant
Public Sub New()
Set m_Observers = New Collection
End Sub
' Subscribe
Public Sub Subscribe(ByVal handler As DataChangedHandler)
m_Observers.Add CLngPtr(handler)
End Sub
' Notify all observers
Private Sub NotifyObservers()
Dim ptr As Variant
For Each ptr In m_Observers
If CLngPtr(ptr) <> 0 Then
Dim h As DataChangedHandler = CType(Of DataChangedHandler)(CLngPtr(ptr))
h(m_Data)
End If
Next ptr
End Sub
' Automatically notify when data changes
Public Property Let Data(ByVal value As Variant)
m_Data = value
NotifyObservers
End Property
End ClassUsage:
Dim src As New cDataSource
src.Subscribe AddressOf OnDataChanged
Public Sub OnDataChanged(ByVal data As Variant)
Debug.Print "Data changed: " & CStr(data)
End SubImportant Notes
1. Delegates Are Not Objects
twinBASIC delegates are value types (function pointers), not objects. Therefore:
' ❌ Wrong — delegates cannot use Set assignment
Set myDelegate = AddressOf SomeFunc
' ✅ Correct — direct assignment
myDelegate = AddressOf SomeFunc
' ❌ Wrong — delegates cannot use Is/IsNot Nothing
If myDelegate IsNot Nothing Then ...
' ✅ Correct — compare with 0 to check if null
If myDelegate <> 0 Then ...
' ❌ Wrong — delegates cannot use Property Set
Public Property Set OnEvent(ByVal handler As MyDelegate)
' ✅ Correct — use Property Let
Public Property Let OnEvent(ByVal handler As MyDelegate)2. Delegates Do Not Support Generics
' ❌ Not supported
Public Delegate Sub Action(Of T)(ByVal item As T)
' ✅ Need to define concrete delegates for each type
Public Delegate Sub LongAction(ByVal item As Long)
Public Delegate Sub StringAction(ByVal item As String)
Public Delegate Sub VariantAction(ByVal item As Variant)3. Calling Convention Must Match
The calling convention (StdCall / CDecl) of the callback function must match the delegate definition:
- Win32 API callbacks typically use
StdCall(the twinBASIC default) - C runtime library callbacks (such as
qsort) useCDecl - Mismatch will cause stack corruption and program crashes
4. Calling an Uninitialized Delegate Will Crash
Calling an uninitialized (value 0) delegate will cause an access violation exception. Always check before calling:
' ❌ Dangerous — crashes if myDelegate is 0
myDelegate(123)
' ✅ Safe — check first
If myDelegate <> 0 Then
myDelegate(123)
End If5. Signature Must Strictly Match
The parameter types, count, passing method (ByVal/ByRef), and return type of the callback function must exactly match the delegate definition, otherwise undefined behavior will occur.
FAQ
Q1: Must a delegate be assigned at declaration?
No. A delegate variable can be declared first and assigned later:
' Option 1: Assign at declaration
Dim myDelegate As Delegate1 = AddressOf Addition
' Option 2: Declare first, assign later
Dim myDelegate As Delegate1
myDelegate = AddressOf Addition
' Option 3: Declare as LongPtr, convert when needed
Dim ptr As LongPtr
ptr = CLngPtr(AddressOf Addition)
' ... convert when calling later
Dim d As Delegate1 = CType(Of Delegate1)(ptr)
d(5, 6)Q2: How to assign a function address (LongPtr) received from external sources to a delegate?
When a function address is passed in as a LongPtr parameter (e.g., in a DLL callback scenario), use CType(Of DelegateType) to convert it to a delegate type:
Public Delegate Sub MyCallback(ByVal data As Long)
' External LongPtr address passed in
Public Sub SetCallback(ByVal callbackAddr As LongPtr)
' Convert directly to delegate and call
Dim cb As MyCallback = CType(Of MyCallback)(callbackAddr)
cb(123)
End SubYou can also store it as LongPtr first and convert when needed:
Private m_Callback As LongPtr
Public Sub SetCallback(ByVal callbackAddr As LongPtr)
m_Callback = callbackAddr ' Store
End Sub
Public Sub DoSomething()
If m_Callback <> 0 Then
Dim cb As MyCallback = CType(Of MyCallback)(m_Callback)
cb(42)
End If
End SubQ3: Can I directly assign a LongPtr to a delegate variable like myDelegate = pFunc?
No, you cannot assign directly. LongPtr cannot be implicitly converted to a delegate type — you must use CType:
' ❌ Compile error — LongPtr cannot be directly assigned to a delegate
Dim myDelegate As Delegate1 = pFunc
' ✅ Correct — use CType for explicit conversion
Dim myDelegate As Delegate1 = CType(Of Delegate1)(pFunc)However, the reverse is true — delegates can be implicitly converted to LongPtr:
' ✅ Correct — delegate can implicitly convert to LongPtr
Dim ptr As LongPtr = CLngPtr(myDelegate)
' Or directly
Dim ptr As LongPtr = myDelegateQ4: How to check if a delegate has been set (is not null)?
Delegates are not objects — you cannot use IsNot Nothing. Compare with 0 instead:
' ❌ Wrong
If myDelegate IsNot Nothing Then ...
' ✅ Correct
If myDelegate <> 0 Then ...
' ✅ Recommended: maintain a boolean flag
Private m_HasHandler As Boolean
Public Property Let OnEvent(ByVal handler As MyDelegate)
m_Ptr = CLngPtr(handler)
m_HasHandler = (handler <> 0)
End PropertyQ5: How to handle callback addresses passed as parameters in DLL wrapping?
This is the most common real-world scenario. The complete pattern is as follows:
' 1. Define callback delegate
Public Delegate Sub ProgressCallback(ByVal percent As Long, ByVal msg As String)
' 2. Receive LongPtr address in exported function
[DllExport]
Public Function DoWork(ByVal progressAddr As LongPtr) As Long
' 3. Store as LongPtr
g_ProgressAddr = progressAddr
' ...do work...
' 4. Convert and call when callback is needed
If g_ProgressAddr <> 0 Then
Dim cb As ProgressCallback = CType(Of ProgressCallback)(g_ProgressAddr)
cb(50, "In progress...")
End If
DoWork = 1
End Function
Private g_ProgressAddr As LongPtrIf the callback address is passed through a UDT structure field, the approach is the same:
Public Type WORK_PARAMS
hwnd As LongPtr
lpProgress As LongPtr ' Callback address as LongPtr field
lpUserData As LongPtr
End Type
Public Sub ProcessWork(ByRef params As WORK_PARAMS)
If params.lpProgress <> 0 Then
Dim cb As ProgressCallback = CType(Of ProgressCallback)(params.lpProgress)
cb(100, "Done")
End If
End SubQ6: Is AddressOf restricted in class modules?
In VB6, AddressOf could only be used in standard modules (.bas). twinBASIC has removed this restriction — AddressOf can be used in class modules, standard modules, and anywhere else. This makes setting delegate callbacks in classes very natural:
Public Class MyClass
Private m_OnResize As LongPtr
Private m_HasResize As Boolean
Public Property Let OnResize(ByVal handler As ResizeHandler)
m_OnResize = CLngPtr(handler)
m_HasResize = (handler <> 0)
End Property
End Class
' Use in any module
Dim obj As New MyClass
obj.OnResize = AddressOf MyResizeHandler ' ✅ Perfectly valid in twinBASICQ7: What is the difference between delegates and WithEvents/Event?
| Feature | Delegate | Event |
|---|---|---|
| Multiple subscriptions | Need to maintain collection manually | Built-in multi-subscription support |
| Syntax | AddressOf + CType | WithEvents + RaiseEvent |
| Type safety | ✅ Compile-time signature checking | ✅ Compile-time signature checking |
| Storable/Passable | ✅ As LongPtr | ❌ Cannot pass events |
| Suitable for DLL callbacks | ✅ Compatible with C APIs | ❌ Not compatible |
| Suitable for observer pattern | ✅ Flexible | ✅ More concise |
Simple rules:
- Internal inter-module communication → use
Event/WithEvents(more concise) - Interaction with external DLL/C APIs → use
Delegate(required) - Need dynamic registration/passing of callbacks → use
Delegate(more flexible)