twinbasic / lang-design

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

Support CDecl calling convention for all internal twinBASIC procedures #25

Open WaynePhillipsEA opened 3 years ago

WaynePhillipsEA commented 3 years ago

Is your feature request related to a problem? Please describe. Some DLL declarations require CDecl calling convention support, which is lacking in VB6 and VBA (except on Mac). twinBASIC now supports CDecl for DLL calls (in v0.9.1750+), but some APIs require callback functions that also use the CDecl calling convention.

Describe the solution you'd like To support the CDecl keyword on all procedure declarations.

Additional context We must offer the same CDecl feature for regular procedure declarations, so that they can be passed as callbacks to APIs that require cdecl callbacks through the AddressOf operator.

Source Email [krool]

wqweto commented 3 years ago

Btw, there's an add-in for VB6 which "fixes" CDecl calling convention on Windows -- VBCDeclFix

WaynePhillipsEA commented 3 years ago

Added in v0.9.1750

Kr00l commented 3 years ago

Thanks for feature update. The API declaration works fine. However, I couldn't figure out how the CDecl callback works.

Private Declare Function snwprintf1 CDecl Lib "msvcrt" Alias "_snwprintf" ( ByVal pszBuffer As Long,  ByVal lCount As Long, ByVal pszFormat As Long, ByRef pArg1 As Any) As Long
    Private Declare Sub qsort CDecl Lib "msvcrt" ( ByRef pFirst As Any, ByVal lNumber As Long, ByVal lSize As Long, ByVal pfnComparator As Long)

    Public Sub Main()
        Dim sBuf    As String
        sBuf = Space$(255)
        msgbox Left$(sBuf, snwprintf1(StrPtr(sBuf), Len(sBuf), StrPtr("Test %ld"), ByVal 123&))
        ' Works great ! :)))

        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

End Module

I tried different ways of declaring the Comparator function. Maybe I missed something.

Alternatives which are not working:

Private Function CDecl Comparator  ( _
                 ByRef a As Long, _
                 ByRef b As Long) As Long
    Comparator = a - b
    End Function
 Private CDecl Function Comparator  ( _
                 ByRef a As Long, _
                 ByRef b As Long) As Long
    Comparator = a - b
    End Function

Or are CDecl callbacks planned at later stage ? Anyhow thanks already.

WaynePhillipsEA commented 3 years ago

Hi @Kr00l, sorry only had time to add the CDecl support for the API declares at the moment. Plus I forgot 😜

I'll re-open this and edit it so that we can add support for all members / callbacks too.

Kr00l commented 3 years ago

Some CDecl API declarations have a vararg. ... Example API:

STRSAFEAPI StringCchPrintfW(
  STRSAFE_LPWSTR  pszDest,
  size_t          cchDest,
  STRSAFE_LPCWSTR pszFormat,
  ...             
);

Would it be technically possible to allow a vararg VB-style declaration in the API declare? For example the As Any is something special, something similar special for vararg would be needed? Just a thought.

WaynePhillipsEA commented 3 years ago

It can be done. We could potentially just use ParamArray () As Any syntax

bclothier commented 2 years ago

A follow up to the request regarding handling the varargs for Cdecl calling syntax. Apparently there are some that accepts a va_list instead of .... Will that be also handled by the ParamArray () As Any as well?

Kr00l commented 2 years ago

For the records. Since v0.13.79 tB now supports ByRef ParamArray Args As Any() which handles va_list and ByVal ParamArray Args As Any() which handles ... for API declares. It is mandatory to state either ByRef or ByVal. (no implicit ByRef possible)

Thus, only support for CDecl callbacks is missing (AddressOf) that tB can handle flat CDecl dll's for all aspects.

So how should the syntax for callback functions look like? I gave above some few examples. Or is it better to have an Attribute for this?

IMO as for API declares it's solved by syntax it would be more harmonic to have it by syntax for functions also. Also the intent looks stronger by language syntax. So for everyone right away to see.

I know that this is low-prio but maybe Wayne can comment if this is an quick missing piece or it has some side-effects to solve also.

bclothier commented 2 years ago

Just to offer an alternative... Instead of decorating a function, we decorate the AddressOf instead:

Private Declare DoIt CDecl Lib "foo.dll" (ByVal callback As LongPtr)
...
DoIt AddressOf CDecl MyCallback
...
Private Function MyCallback()
...
End Function

The advantage of that approach is that it avoids polluting the language with functions that requires calling conventions to be used, making it easy and simple for me to directly call my MyCallback without any change in the syntax. The compiler is then free to generate the necessary stub to wrap the MyCallback to comply with the cdecl calling convention.

WaynePhillipsEA commented 2 years ago

Yes, there's two approaches here... either change the real procedure to use CDecl calling convention, or generate small stubs when needed at the callsite to fixup cdecl callbacks to the real stdcall convention. Handily, it only affects 32bit as CDecl is already the calling convention on 64-bit.

Kr00l commented 2 years ago

Yes, there's two approaches here... either change the real procedure to use CDecl calling convention, or generate small stubs when needed at the callsite to fixup cdecl callbacks to the real stdcall convention. Handily, it only affects 32bit as CDecl is already the calling convention on 64-bit.

Two approaches.. ok, why not allowing the two in paralell? It would allow native CDecl functions or with the stub forward. Scenarios:

  1. CDecl Function with normal AddressOf = CDecl
  2. Normal Function with CDecl AddressOf = CDecl (Stub overhead)
  3. Normal Function with Normal AddressOf = StdCall on 32 or CDecl on 64
  4. CDecl Function with CDecl AddressOf = CDecl
bclothier commented 2 years ago

FWIW, I am not sold on the idea of being able to specify calling conventions within the functions. For one, it seems to go against the grain of BASIC -- we want to focus on the solution, not implementation. That kind of specification belongs at the boundaries, which would be the Declare statement and somewhat indirectly AddressOf operator. Within the language, there should be no need to even think about it.

Even in .NET, we don't get to control calling convention except via the DllImport and UnmanagedFunctionPointer attributes.

BTW, looking at the UnmanagedFunctionPointer, it seems to me that it would be far more elegant to handle calling conventions for callback by introducing delegates to tB (discussed in twinbasic/lang-design#44). That way it stays at the boundary where it belongs.

WaynePhillipsEA commented 2 years ago

My thoughts;

  1. It's easier to implement on the function itself. It's not majorly difficult to implement stubs when encountering AddressOf... but still it's easier the other way round.
  2. I can envisage wanting/needing other calling conventions support, for example FASTCALL. With FASTCALL, the first two arguments are passed in registers, often giving a slight performance benefit. Using stubs for that one would obliterate the performance benefit.
  3. If we only implement stubs at the AddressOf point, then we can't implement interfaces that may have CDecl members. Admittedly you don't see them very often in type libraries, but it is possible to create a COM interface with members that use CDecl/Fastcall.
bclothier commented 2 years ago

I didn’t consider the fact that a type library allows for different calling conventions. A follow up question would be whether allowing different calling conventions to be used among other tB procedures would negatively affect tB performance or stability. I was thinking that restricting it to the boundary would avoid issues. Is that true?

WaynePhillipsEA commented 2 years ago

CDecl is a very small tweak to the function epilogue, and so shouldn't affect much at all since we already support CDecl at the call sites for DLLs. It will require more tests to be written than the AddressOf version though, since we will want to make sure that generated type libraries are correct, and that we can consume existing type-libraries that have CDecl members.

Kr00l commented 2 years ago

Few more comments..

The CDecl syntax keyword will anyhow be ignored on x64 build. So that's an easy going for x64 at all. It's just a necessity for x86 build targets. What I want to say : a project using this syntax on x86 can migrate to x64 without refactoring something.

The CDecl syntax is optional. If it will get misused by the AddressOf operator or the function itself is no difference.

Concerning the two way approach. Effectively we normally would like to avoid it. Because for many it would not be clear about the difference. So IMO a native solution is in favor of something simulated.

loquat commented 1 year ago

when will cdecl callbacks be support?

WaynePhillipsEA commented 1 year ago

As of BETA 287, the CDecl calling convention is now supported for all internal procedures, so for example, you can now define a CDecl callback:

Function MyCDeclCallback CDecl(ByVal A As Long)
End Function

And thus using AddressOf MyCDeclCallback now gives you a function pointer that is CDecl compatible.

(note: syntax copied to match the existing CDecl support in declare statements, where the CDecl keyword is expected immediately following the procedure name)

loquat commented 1 year ago

have some test, it is working.