Open Kr00l opened 3 years ago
FWIW, I would treat this as a request for delegate support, which was cited in twinbasic/twinbasic#5 and alluded to in twinbasic/lang-design#27.
@bclothier I actually disagree with that. Although invoking pointers can provide support for delegates, delegates cannot provide support for invoking all pointers.
See Universal DLL Calls for examples of invoking pointers. I feel a bigger feature request here is:
Ability to invoke any pointer with any calling convention
Dim ptr as LongPtr
ptr = AddressOf myFunc
retVar = ptr.stdInvoke(argtypes, argvalues)
ptr = getCFunctionPointer()
ptr.invoke(CC_CDECL, argtypes, argvalues)
ptr = objptr(something)
ptr.invokeObject(vtableoffset, argtypes, argvalues)
For information - call conventions it'd be great to support:
Public Enum CALLINGCONVENTION_ENUM
' http://msdn.microsoft.com/en-us/library/system.runtime.interopservices.comtypes.callconv%28v=vs.110%29.aspx
CC_FASTCALL = 0&
CC_CDECL
CC_PASCAL
CC_MACPASCAL
CC_STDCALL ' typical windows APIs
CC_FPFASTCALL
CC_SYSCALL
CC_MPWCDECL
CC_MPWPASCAL
End Enum
Standard pointer-to-function delegates look very similar to an API function declare as syntax. It's an API function with user-provided pfn to call to while the API declare has to keep internal book-keeping about retrieving hModule and pfn on first call.
Instance pointer-to-method delegates are a different beast altogether.
Since I started this feature request I suggest my "syntax" proposal.
Like in a API declare, but instead of the "Lib" keyword the keyword "CallByAddressOf" is used. It's a keyword to declare a function "prototype" with it's signature only.
Those "APIs" with the keyword CallByAddressOf needs in it's first argument "automagically" the pfn. (AddressOf, "ProcPtr")
Example A: (invoke dll func pointer)
Private Declare Function MyGetDC CallByAddressOf ( ByVal hWnd As Long) As Long
Dim pfn As Long
pfn = GetProcAddress(LoadLibrary("user32"), "GetDC")
Dim hDC As Long
hDC = MyGetDC(pfn, hWnd)
Example B: (invoke VB address pointer)
Private Declare Sub MyVBSub CallByAddressOf ( ByRef szText As String, ByVal szAdd As String)
Dim pfn As Long
pfn = ProcPtr(AddressOf VBSub)
MyVBSub pfn, Text1, Text2
Private Function ProcPtr(ByVal Addr As Long) As Long
ProcPtr = Addr
End Function
Private Sub VBSub(ByRef szText As String, ByVal szAdd As string)
szText = szText & szAdd
End Sub
The 'CallByAddressOf' meaning is already contained in the 'Declare' Keyword.
E.g. in FreeBasic your Example A could be written:
Private Declare Function MyGetDC (ByVal hWnd As Long) As Long
Dim SomeHdc As Long
If MyGetDC = 0 Then MyGetDC = DylibSymbol(DylibLoad("user32"), "GetDC")
SomeHdc = MyGetDC(hWnd)
(DylibLoad, DylibSymbol, DylibFree being runtime-functions, which abstract from the underlying system-APIs)
And your example B could be written this way:
Private Declare Sub MyVBSub (ByRef szText As String, ByVal szAdd As String)
If MyVBSub = 0 Then MyVBSub = AddressOf VBSub '<- the compiler recognizes what is meant, due to the AddressOf Keyword
MyVBSub Text1, Text2
Private Sub VBSub(ByRef szText As String, ByVal szAdd As string)
szText = szText & szAdd
End Sub
There's also recognition at DIM-level for Function-Signature-Defs, like: Dim MyVBSub As Sub(ByRef szText As String, ByVal szAdd As String) (the above is working in a similar way also for Function-Defs in UDTs)
There's another thing I found immediately "nice to have" when I played with FreeBasic.
In addition to "right-hand-side" Type-specifying, it allows to define Vars also "left-typed":
Dim As Long i
Especially nice with that is, that now a sequence of vars with the same type can be detected unambigously, when written thus:
Dim As Long i, j, k
Or within a UDT:
Type t3DPtDbl
As Double x, y, z, w
End Type
I'd suggest - when new Syntax (not existing in VB6) is considered for some new feature in twinBasic, to first look for "prior art" in the following "compatibility-order":
Olaf
Thanks Olaf for your input. Yes, your syntax looks cleaner by omitting 'CallByAddressOf' and of course omit the 'Lib' keyword. So just a blank 'Declare' would be sufficient.
Also to assign the 'pfn' like a 'property let' is cool, since then it needs to be assigned only once. I was not aware of this "prior art" as I don't use FreeBasic. But is I think/hope that Wayne is aware to lookout of these. :-)
This does boil down to the need for pointer-to-function delegates, as they provide for some type-safety.
The plan is that AddressOf will be modified to return a typed delegate, but for backwards compatibility this will be allowed to coerce to LongPtr [Long on win32].
@vbRichClient The inlined definition of delegates in FreeBasic does look quite nice. The 'Dim As {type} x, y, z' syntax deserves it's own feature request for discussion.
I am not sure how I feel about the AddressOf
being modified to allow assignment to a LongPtr
variable/argument. That seems to open an avenue for accidental implicit conversion, wouldn't it?
As mentioned elsewhere, I am not a fan of implicit conversions VB* likes to do as that usually hides potential bugs that becomes runtime errors. In .NET delegates are not equivalent to a function pointer and cannot be assigned as such except across a p/invoke and usually needs MarshalAs(UnmanagedType.FunctionPointer)
IIRC. The good thing is that we never need to do if(ptr) ptr(...)
; we just ptr.Invoke(...)
and all the housekeeping stuff is tidily abstracted out.
Regarding delegates, one other thing I want to be sure to consider is that they will be treated as a type, and not as some kind of statement. Treating them as a type allows reuse and sharing and would also allow for generics support as well.
@bclothier Yes, delegates will be treated as a typed function pointer, internally treated as a new datatype.
We will have to support coercing to LongPtr for backwards compatibility. We can definitely restrict that when Option Strict is on though!
should this be moved to lang-design and converted to a discussion?
I have found some types of freebasic function pointer syntaxes at wikis below https://www.freebasic.net/wiki/KeyPgFunctionPtr https://documentation.help/FreeBASIC/ProPgProcedurePointers.html https://www.freebasic.net/wiki/KeyPgOpProcptr they are really far too flexible for vbers.
what i think maybe we can add function pointer support follow these steps Step 1.Basic support '----------------------syntax1---------------------- Sub MySub(ByVal x As Long) Debug.Print x End Sub Dim pSub1 As [Cdecl|Stdcall|Pascal|Syscall] Sub(x as long) = AddressOf mySub Call pSub(123) ' Function myFunction(ByVal x as long) as long Return x End Function Dim pFunc1 As [Cdecl|Stdcall|Pascal|Syscall] Function(x as long) as long = ProcPtr(myFunction) Or Dim pFunc1 As [Cdecl|Stdcall|Pascal|Syscall] Function(x as long) as long = p_myFunction 'which p_myFunction get from any dlls Debug.Print pFunc1(123) ' '----------------------syntax2---------------------- Declare Function Subtract( x As Integer, y As Integer) As Integer Declare Function Add( x As Integer, y As Integer) As Integer myFunction = ProcPtr(Add) Debug.Print myFunction(2, 3) ' Or AddressOf Add myFunction = ProcPtr(Subtract) ' Or AddressOf Subtract Debug.Print myFunction(2, 3) Function Add( x As Integer, y As Integer) As Integer Return x + y End Function Function Subtract( x As Integer, y As Integer) As Integer Return x - y End Function
maybe both syntaxes of function pointer declarations should be consider
Step 2.Callback support
'----------------------example1----------------------
Function x2 (ByVal i As Integer) As Integer
Return i * 2
End Function
Function operation (ByVal i As Integer, ByVal op As Function (ByVal As Integer) As Integer) As Integer
Return op(i)
End Function
'
Debug.Print operation(4, AddressOf x2)
'
'----------------------example2----------------------
'thread Sub definition
Sub threadInkey (ByVal p As Any Ptr)
If p > 0 Then '' test condition callback Function defined
Dim As Function (ByRef As String) As Integer callback = p '' convert the any ptr to a callback Function pointer
Do
Dim As String s = Inkey
If s <> "" Then '' test condition key pressed
If callback(s) Then '' test condition to finish thread
Exit Do
End If
End If
Sleep 50, 1
Loop
End If
End Sub
'' user callback Function definition
Function printInkey (ByRef s As String) As Integer
If Asc(s) = 27 Then '' test condition key pressed =
'' user main code Dim As Any Ptr p = ThreadCreate(@threadInkey, @printInkey) '' launch the thread, passing the callback Function address ThreadWait(p)
Maybe at this stop we can consider adding some new types such as AnyPtr which is AnyPtr2 which is AnyPtr3 which is ……
3.Other support Such as Types Type parent Extends Object Dim As String s1 Dim As String s2 Declare Virtual Sub test() Declare Virtual Operator Cast() As String End Type ……
There's an existing VB6 hack that uses syntax
Declare Function CallMeByPtr Lib * (...) As ...
Then you can assign CallMeByPtr = SomeLongPtr
A profoundly brilliant hack from firehacker on VBStreets. It's the most simple, clear syntax I've seen for adding call by pointer to the language. Would be my choice for the syntax tB uses to implement it.
The tB solution will be similar but ultimately more flexible. Something like:
Declare Delegate Function CallMe (...) As ...
Then:
Dim myProc As CallMe
myProc = CType(Of CallMe)(SomeLongPtr)
myProc(...)
This way you've not got a 1:1 mapping of definition and value as per the VB6 hack. The delegate is basically a function-type (backed with a LongPtr of course).
FWIW, I'm strongly in favor of delegate-based design than tossing around naked pointers. The former can be statically analyzed and validated for code correctness. The latter cannot be. Even in the case where there might be no well-defined "interface" (for a lack of better term), a generic Action
in the form of lambda expression with closures is also easy to statically analyze for correctness. There should be no need to streak in tB. :)
Delegate like dotnet will be just fine. From FB's "Dim p as Any Ptr Ptr", I thought of "Dim p as AnyPtr2" Would this be in consideration? or should i add a new feature?
Would delegates require user mode API-backing? Needing call by pointer is far more common in native mode.
Nope, just a simple assembled indirect call.
Delegate syntax now supported in BETA 616. Experimental.
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 Function
What is a delegate? -- it's a function pointer type. It's just a LongPtr
type that the compiler can associate with a function definition, allowing you to call function pointers directly in tB, without needing to go through DispCallFunc
and other such high level functions.
In a future update (soon), AddressOf
will return a delegate type rather than LongPtr, so that compiler warnings about mismatched conversions will be avoidable when you have correct matching delegate definitions.
Delegate support is more refined in BETA 617. In particular, the AddressOf
operator now returns a delegate type, rather than a generic LongPtr, for both class-instance and non-class based AddressOf syntax.
This means that the compiler can now help promote type safety by ensuring delegate types match as the function pointers are passed around (i.e. correct params, return type and calling convention) and this type checking also occurs when you use AddressOf to assign to a proper delegate type rather than the generic LongPtr type.
For backwards compatibility reasons, coercing to a standard LongPtr is of course supported, but coercing from a LongPtr into a explicit delegate type should use CType(Of delegateType)(longPtrValue)
syntax to avoid compiler warnings. Such compiler warnings can be converted to errors if you prefer to be more strict in usage.
Now that we have delegates, I want to consider if we can go one better. In .NET, it annoys that I have to re-define the delegate & callback:
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 _
)
Private Function Comparator CDecl( _
ByRef a As Long, _
ByRef b As Long _
) As Long
Comparator = a - b
End Function
...
qsort z(0), UBound(z) + 1, LenB(z(0)), AddressOf Comparator
What I would like to do is maybe something like:
Private LongComparator Comparator
'<define the callback
End LongComparator
The idea being that instead of repeating the prototype, it can be defined in one place with the delegate and then referenced. The tB IDE can then show the prototype as a CodeLen feature so that we still see something like:
Private LongComparator Comparator ('Function Comparator CDecl( _
ByRef a As Long, _
ByRef b As Long _
) As Long')
in the IDE. That would then eliminate the need to copy'n'paste the prototype between the delegate definition and the functions meant to be used for the delegate.
One of the major benefits of delegates is actually seeing BASIC syntax for the callback or wndproc you need to write... Don't think it should be hidden away again in favor of an opaque prototype that's completely different from how every other method is defined in the language... There's a big usability and learning curve penalty for breaking an otherwise completely consistent, omnipresent pattern.
Edit: What might be helpful, since the prototype is known, have them in the left or middle drop-down for the active editor, then they can be selected and autogenerated like an event or interface method. That would save needing to retype without the drawbacks of a completely new way of defining methods.
Yeah an intellisense support for auto completing a prototype would work too. The downside is that if the delegate's signature is altered, the functions would need to be updated. If the function is directly assigned, the compiler should have no trouble flagging the now mismatched prototype but a quick fix would be needed to make it easy and wouldn't help in the cases where the assignments are indirect.
The drop-downs on the top may not be the best place, though as it can be possible to define delegate in one place and use it on an entirely another place. In Visual Studio when adding an event, it offers to both auto complete the delegate assignment as well create an event handler stub. Something similar can be achieved but that would be limited to the flow of creating a delegate, set up the assignment and then define the callback. The alternate flow of defining the delegate, defining the callback procedure then finally setting up the assignment would not be covered.
The original idea would work no matter the flow being used and would be always explicit allowing the compiler to flag the mismatched prototype even if it wasn't yet assigned. I agree with your criticism that it's not good to make it opaque which is why I thought of using the CodeLens feature to avoid this.
Is your feature request related to a problem? Please describe. VB6/VBA cannot invoke functions by a Pointer in a native way like it is done in C/C++.
Describe the solution you'd like A VB-Ish syntax is needed. (shall be discussed) Sure is that the function "template" needs to be stated in the syntax that shall be invoked.
Describe alternatives you've considered Often the workaround of using CallWindowProc is used. Or better the low-level API DispCallFunc. But if it can be avoided then it is "nice to have".
Additional context Low-priority as only really needed in edge cases.