twinbasic / lang-design

Language Design for twinBASIC
MIT License
11 stars 1 forks source link

Invoke function by AddressOf Pointer #44

Open Kr00l opened 3 years ago

Kr00l commented 3 years ago

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.

bclothier commented 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.

sancarn commented 3 years ago

@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
bclothier commented 3 years ago

In C#, you can specify the calling conventions for your delegates.

wqweto commented 3 years ago

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.

Kr00l commented 3 years ago

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
vbRichClient commented 3 years ago

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

Kr00l commented 3 years ago

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. :-)

WaynePhillipsEA commented 3 years ago

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.

bclothier commented 3 years ago

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.

WaynePhillipsEA commented 3 years ago

@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!

mansellan commented 2 years ago

should this be moved to lang-design and converted to a discussion?

loquat commented 1 month ago

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 = Print Return -1 '' order thread to finish Else Print s; Return 0 '' order thread to continue End If End Function

'' 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 ……

fafalone commented 1 month ago

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.

WaynePhillipsEA commented 1 month ago

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).

bclothier commented 1 month ago

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. :)

loquat commented 1 month ago

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?

fafalone commented 1 month ago

Would delegates require user mode API-backing? Needing call by pointer is far more common in native mode.

WaynePhillipsEA commented 1 month ago

Nope, just a simple assembled indirect call.

WaynePhillipsEA commented 1 month ago

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.

WaynePhillipsEA commented 1 month ago

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.

bclothier commented 1 month ago

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.

fafalone commented 1 month ago

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.

bclothier commented 1 month ago

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.