twinbasic / lang-design

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

Support inline asm (separate from static linking) #65

Open fafalone opened 11 months ago

fafalone commented 11 months ago

Was surprised to see this only discussed in reference as a use for static linking ability; thought there was already a formal request (if I've somehow missed it, apologies, but I searched issues/discussions here and /twinbasic).

I propose tB should support native syntax for inline assembly. tB eliminates some use cases for this, but could never anticipate all of them.

The most appropriate option, I think, it to allow it within functions; whole modules could be covered by static linking. There would be a traditional declaration, followed by function that may also contain tB code, that allows syntax to begin and end a block of assembly.

There's a number of good options for syntax; I propose the best way is to have a line starting with a character that would otherwise be illegal as a first, to avoid conflicts with existing names.

Public Function ThisHasASM() As Long
Call SomethingElse
@ASM
assembly here
@End ASM
End Function

The end syntax matches traditional block end syntax, and if there's still plans to change attribute syntax to @? If attributes inside methods would be too difficult, well the leading character isn't too important so long as it's not valid as a first character for any other use.


While it's certainly 'not BASIC' being an actual different language, I'd again point to the current state of usage: For the rare cases where no suitable alternatives exist, we already have the situation of VBx using assembly through several methods, so this is not introducing something to the language entirely without precedent and current examples. The trick has an inline asm addin for VB6, but more commonly, it's seen as a bunch of inscrutable magic hex values written to an executable location or called with DispCallFunc/CallWindowProc. So there's no avoiding the fact that it will be used in tB just like it's used in VBx-- in fact I tested a project using asm thunks in tB just the other day, it worked fine.\ So it's not a question of whether this can be kept out, it's a question of should it be made friendlier and more accessible, thus increasing tB's power and ease of use for advanced programming? I see no reason why not. Especially given that it's sufficiently difficult only the most skilled and experienced programmers would use it, making it unlikely we'd see a bunch of junk asm code mucking things up for beginners, because it's rare and not even approachable until you have the sense to understand when it should be used.

sokinkeso commented 11 months ago

I think this has been discussed again . I said my opinion in a post here or in Discord ( I don't remember). Inline assembly is a "must", in a language like tB, that tends to cover low and high level applications combining the ease of vb6 and the power of c++. So , IMO , inline assembly should finally included..but not sure if this is a v1.0 target..

fafalone commented 11 months ago

It's been mentioned in a few places for sure but I thought it was time it had a form language design proposal issue.

Now granted this is coming from a position of pretty much total ignorance, but it seems like it should be easy to implement... after all, you don't need to do any real processing, nobody is asking for asm intellisense or autocomplete or even syntax highlighting (at least not before like 3.0 maybe)... you would just need to take the contents of that block, feed it through to the last step of compiling (asm->exe) in the right spot. It's not like VB where there's no access to the compiler and linker sources so it has to be hacked in through intercepting and modifying obj files.

Wayne mentioned he's written his own C compiler too... it probably wouldn't be too far out of the way to implement inline C at the same time too. ThunderVB has that feature. In fact it even has example code about the recent OpenGL conversations...

Public Sub InitSDL()
'#c'lib=C:\sdl\lib\SDL.lib
'#c'lib=GLU32.LIB
'#c'lib=GLAUX.LIB
'#c'lib=OPENGL32.LIB
'#c'
'#c'  //SDL OpenGL Tutorial.
'#c'  //(c) Michael Vance, 2000
'#c'  //briareos@lokigames.com
'#c'  //
'#c'  //Distributed under terms of the LGPL.
'#c'  //
'#c' #include <windows.h>
'#c' #include "C:\sdl\include\SDL.h"
'#c' #include <GL/gl.h>
'#c' #include <GL/glu.h>
'#c'
'#c' #include <stdio.h>
'#c' #include <stdlib.h>
'#c' static int exit_app=0;
'#c' static GLboolean should_rotate = GL_TRUE;
'#c'
'#c' static void quit_tutorial( int code )

It goes on with a larger demo of using OpenGL with inline C that imports C headers for OpenGL.

loquat commented 6 days ago

if this kind of features should be support in future, maybe consider it as a extra secondly-develop method/interface for tBers to add any other codes in tB module/class, which will be more convenient for some of the expert coders to add call/callback abilities of other coding language, such as c/cpp/go/java/php/even js, into tB.

loquat commented 6 days ago

function func(p1,p2) dim b as long dim c as longptr '@c_file $apppath$\demo.c //include an c file

if Win64 then

'@asm_x64_start ' asmcode '@asm_x64_end b = p1 xor p2

Elseif Win32 then

end if

end function

fafalone commented 6 days ago

Note you can already use function calls from .c or .asm files by compiling them to .obj or .lib and using tB's static linking ability to build it into the exe. See Samples 17 and 18, or WinDevLib's wdAPIInterlocked.twin for examples.

loquat commented 6 days ago

Note you can already use function calls from .c or .asm files by compiling them to .obj or .lib and using tB's static linking ability to build it into the exe. See Samples 17 and 18, or WinDevLib's wdAPIInterlocked.twin for examples.

but it seems static linking ability can not support mix coding? one line basic code one line asm code one line c code

fafalone commented 6 days ago

Just function calls like APIs. But the point is between tBs new language features and that, it covers a lot use cases for inline asm/c, a good alternative until true inline is supported.

wqweto commented 5 days ago

What is a low hanging fruit here until inline ASM feature lands (or some inter-language preprocessor implemented) is to just add an Emit(b1, b2, b3, …, bn) intrinsic like some C/C++ compilers support which directly emits instruction bytes in codegen i.e. similar to db directive in native assemblers.

This usually coupled with naked function attribute to skip standard stack-frame prolog/epilog codegen and conditional compilation on Win64 const would allow embedding any ASM thunk as ordinary TB code without separate .obj files.

WaynePhillipsEA commented 5 days ago

@wqweto that's low hanging fruit indeed!

WaynePhillipsEA commented 5 days ago

@wqweto perhaps EmitBytes(), EmitIntegers(), EmitLongs(), EmitLongLongs()?

Greedquest commented 5 days ago

Would EmitLongPtrs also would be useful? 64 bit asm and 32 bit tend to be so different that maybe you wouldn't do it that way.

WaynePhillipsEA commented 5 days ago

Probably not so useful, as you're extremely likely to need to write conditionally compiled code to differentiate here anyway.

wqweto commented 5 days ago

Having a whole family of functions for something quite obscure might be an overkill as original __emit__ "pseudo-function that injects literal values directly into the object code" in C/C++ supports bytes only.

Some links:

WaynePhillipsEA commented 5 days ago

My thinking was that it could make things easier for when working with non hard coded offsets and values. But yes, it might be overkill.

    EmitBytes(&H8B, &H81) : EmitLongs(VTableOffsetOf MyProc) ' mov eax, dword [ecx+X]
    EmitBytes(&HFF, &HD0)   ' call eax

edit: (modified my example due to use of relocation which wouldn't be supported here)

wqweto commented 4 days ago

Yes, this seems useful, considering the params had to be literals in original proposal. A lot of folks wouldn't understand why Emit(Idx + 42) would not work for this "function".

Another option is to keep a single pseudo-function name but emit whatever typed literal is passed so this can work for Currency too withoutlittering the global namespace with too much symbols.

WaynePhillipsEA commented 4 days ago

Another option is to keep a single pseudo-function name but emit whatever typed literal is passed so this can work for Currency too withoutlittering the global namespace with too much symbols.

Yes, I did consider that, but the problem is that small literals have a natural type of Integer, e.g. &H42 would then be output as two-bytes. We could say that values <256 are output as a Byte, but that creates further confusion as you might actually intend/need the smaller value to be output as an Integer/Long etc (particularly when using non hard-coded offsets, that would get confusing)

WaynePhillipsEA commented 4 days ago

I've currently got this working:

Function DoubleUp Naked(ByVal Value1 As Long) As Long
        #If Win64 = False Then
            Const Me_StackOffset = 4                        ' implicit Me arg for class methods
            Const Value1_StackOffset = 8
            Const OutPtr_StackOffset = 12                   ' implicit OUT arg
            Emit(&H8B, &H44, &H24, Value1_StackOffset)      ' mov eax, dword ptr [esp + 8]          (eax = Value1)
            Emit(&HD1, &HE0)                                ' shl eax, 1                            (eax = Value1 * 2)
            Emit(&H8B, &H54, &H24, OutPtr_StackOffset)      ' mov edx, dword ptr [esp + 12]         (edx = ptr OUT)
            Emit(&H89, &H02)                                ' mov dword ptr [edx], eax              (*OUT = Value1 * 2)        
            Emit(&H31, &HC0)                                ' xor eax, eax                          (eax = S_OK)
            Emit(&HC2, &H0C, &H00)                          ' ret 12
        #Else
                                                            ' rcx == implicit Me arg for class methods
                                                            ' rdx == Value1
                                                            ' r8 == implicit OUT arg
            Emit(&HD1, &HE2)                                ' shl edx, 1                            (edx = Value1 * 2)
            Emit(&H41, &H89, &H10)                          ' mov dword ptr [r8], edx               (*OUT = Value1 * 2)        
            Emit(&H31, &HC0)                                ' xor eax, eax                          (eax = S_OK)
            Emit(&HC3)                                      ' ret
        #End If
    End Function

And this for the standard-module version:

Function DoubleUp Naked(ByVal Value1 As Long) As Long
        #If Win64 = False Then
            Const Value1_StackOffset = 4
            Emit(&H8B, &H44, &H24, Value1_StackOffset)      ' mov eax, dword ptr [esp + 4]          (eax = Value1)
            Emit(&HD1, &HE0)                                ' shl eax, 1                            (eax = Value1 * 2)
            Emit(&HC2, &H04, &H00)                          ' ret 4                                 (return value in EAX)
        #Else
                                                            ' rcx == Value1
            Emit(&HD1, &HE1)                                ' shl ecx, 1                            (ecx = Value1 * 2)
            Emit(&H89, &HC8)                                ' mov eax, ecx                          
            Emit(&HC3)                                      ' ret                                   (return value in EAX)
        #End If
    End Function
WaynePhillipsEA commented 4 days ago

You can now play around with this, in BETA 605. Tip: to debug the emitted code, attach a native debugger (e.g. VS, or x64dbg) and use Emit(&HCC) to insert a debugger breakpoint (INT3) in your codegen.

wqweto commented 4 days ago

Wondering if it is possible to default to Byte for &H42 suffixless literals and require % for Integer ones (less than 256) only here, the way it's currently impl for small Long literals? Might be a less surprising mode of operandi for this pseudo-function.

WaynePhillipsEA commented 4 days ago

Wondering if it is possible to default to Byte for &H42 suffixless literals and require % for Integer ones (less than 256) only here, the way it's currently impl for small Long literals? Might be a less surprising mode of operandi for this pseudo-function.

Might be difficult to achieve without it becoming confusing (e.g. if the value comes from a Const). Needs some thought.

WaynePhillipsEA commented 4 days ago

Correction to above: where you see: Emit(&HC2, &H0C) ' ret 12 Emit(&HC2, &H04) ' ret 4 it should have been Emit(&HC2, &H0C, &H00) ' ret 12 Emit(&HC2, &H04, &H00) ' ret 4.
(I will correct the post)

WaynePhillipsEA commented 4 days ago

Another example, using the CPUID instruction (in a standard module):

    Public Function GetCPUID_ProcessorBrandString() As String
        Dim ProcessorBrandStringANSI As String = Space$(24)
        GetCPUID_ProcessorBrandStringANSI(ProcessorBrandStringANSI)
        Return StrConv(ProcessorBrandStringANSI, vbUnicode)
    End Function

    Private Sub GetCPUID_ProcessorBrandStringANSI Naked(ByVal PreAllocatedString As String)

        ' Use the extended CPUID instruction to get the CPU ProcessorBrandString.
        '  You must pass in a String capable of holding a 48-byte ANSI string
        '  TODO: checking to ensure the CPU supports the CPUID instruction

        #If Win64 = False Then

            Emit(&H55)                                          ' push ebp                              (preserve non-volatile register)
            Emit(&H53)                                          ' push ebx                              (preserve non-volatile register)

            Const Value1_StackOffset = 12
            Emit(&H8B, &H6C, &H24, Value1_StackOffset)          ' mov ebp, [esp+12]                     (ebp == PreAllocatedString)

            Emit(&HB8, &H02, &H00, &H00, &H80)                  ' mov eax, 0x80000002
            Emit(&H0F, &HA2)                                    ' cpuid        
            Emit(&H89, &H45, &H00)                              ' mov dword ptr [ebp], eax              (ANSI 4 chars)
            Emit(&H89, &H5D, &H04)                              ' mov dword ptr [ebp+4h], ebx           (ANSI 4 chars)
            Emit(&H89, &H4D, &H08)                              ' mov dword ptr [ebp+8h], ecx           (ANSI 4 chars)
            Emit(&H89, &H55, &H0C)                              ' mov dword ptr [ebp+Ch], edx           (ANSI 4 chars)

            Emit(&HB8, &H03, &H00, &H00, &H80)                  ' mov eax, 0x80000003
            Emit(&H0F, &HA2)                                    ' cpuid        
            Emit(&H89, &H45, &H10)                              ' mov dword ptr [ebp+10h], eax          (ANSI 4 chars)
            Emit(&H89, &H5D, &H14)                              ' mov dword ptr [ebp+14h], ebx          (ANSI 4 chars)
            Emit(&H89, &H4D, &H18)                              ' mov dword ptr [ebp+18h], ecx          (ANSI 4 chars)
            Emit(&H89, &H55, &H1C)                              ' mov dword ptr [ebp+1Ch], edx          (ANSI 4 chars)

            Emit(&HB8, &H04, &H00, &H00, &H80)                  ' mov eax, 0x80000004
            Emit(&H0F, &HA2)                                    ' cpuid        
            Emit(&H89, &H45, &H20)                              ' mov dword ptr [ebp+20h], eax          (ANSI 4 chars)
            Emit(&H89, &H5D, &H24)                              ' mov dword ptr [ebp+24h], ebx          (ANSI 4 chars)
            Emit(&H89, &H4D, &H28)                              ' mov dword ptr [ebp+28h], ecx          (ANSI 4 chars)
            Emit(&H89, &H55, &H2C)                              ' mov dword ptr [ebp+2Ch], edx          (ANSI 4 chars)

            Emit(&H5B)                                          ' pop ebx                               (restore non-volatile register)
            Emit(&H5D)                                          ' pop ebp                               (restore non-volatile register)
            Emit(&HC2, &H04, &H00)                              ' ret 4                                 

        #Else

            Emit(&H53)                                          ' push ebx                              (preserve non-volatile register)
            Emit(&H49, &H89, &HCB)                              ' mov r11, rcx                          (rcx will be overwritten by CPUID)

            Emit(&HB8, &H02, &H00, &H00, &H80)                  ' mov eax, 0x80000002
            Emit(&H0F, &HA2)                                    ' cpuid        
            Emit(&H41, &H89, &H43, &H00)                        ' mov dword ptr [r11], eax              (ANSI 4 chars)
            Emit(&H41, &H89, &H5B, &H04)                        ' mov dword ptr [r11+4h], ebx           (ANSI 4 chars)
            Emit(&H41, &H89, &H4B, &H08)                        ' mov dword ptr [r11+8h], ecx           (ANSI 4 chars)
            Emit(&H41, &H89, &H53, &H0C)                        ' mov dword ptr [r11+Ch], edx           (ANSI 4 chars)

            Emit(&HB8, &H03, &H00, &H00, &H80)                  ' mov eax, 0x80000003
            Emit(&H0F, &HA2)                                    ' cpuid        
            Emit(&H41, &H89, &H43, &H10)                        ' mov dword ptr [r11+10h], eax          (ANSI 4 chars)
            Emit(&H41, &H89, &H5B, &H14)                        ' mov dword ptr [r11+14h], ebx          (ANSI 4 chars)
            Emit(&H41, &H89, &H4B, &H18)                        ' mov dword ptr [r11+18h], ecx          (ANSI 4 chars)
            Emit(&H41, &H89, &H53, &H1C)                        ' mov dword ptr [r11+1Ch], edx          (ANSI 4 chars)

            Emit(&HB8, &H04, &H00, &H00, &H80)                  ' mov eax, 0x80000004
            Emit(&H0F, &HA2)                                    ' cpuid        
            Emit(&H41, &H89, &H43, &H20)                        ' mov dword ptr [r11+20h], eax          (ANSI 4 chars)
            Emit(&H41, &H89, &H5B, &H24)                        ' mov dword ptr [r11+24h], ebx          (ANSI 4 chars)
            Emit(&H41, &H89, &H4B, &H28)                        ' mov dword ptr [r11+28h], ecx          (ANSI 4 chars)
            Emit(&H41, &H89, &H53, &H2C)                        ' mov dword ptr [r11+2Ch], edx          (ANSI 4 chars)

            Emit(&H5B)                                          ' pop ebx                               (restore non-volatile register)
            Emit(&HC3)                                          ' ret                                 

        #End If

    End Sub
WaynePhillipsEA commented 3 days ago

BETA 607 adds EmitAny() and StackOffset() support. EmitAny is the same as Emit() except that the output length of each element is inferred from the datatype. In other words, if you pass a Long literal value, e.g. 42&, it will output 4 bytes into the codegen stream. StackOffset() allows you to obtain the stack offset of a local variable or parameter, most useful for non-Naked procedures where you don't control local-variable positions in the stack frame, and so need a way to determine this for Emit() purposes.

Two examples:

    Public Sub TestWriteToLocalVar1()
        Dim Foo As Long = &H12345678
        ' this assembled code is valid for both X86 and X64
        Emit(&HC7, &H84, &H24): EmitAny(StackOffset(Foo), 42&)      ' mov dword ptr [esp + StackOffset(Foo)], &H0000002A
        MsgBox Foo          ' Foo has been populated with the value 42
    End Sub

    Private Sub TestLocalVar2a(ByVal ParamVar1 As Long)
        Dim Foo As Long = &H12345678
        ' this is valid in both X86 and X64
        Emit(&H8B, &H84, &H24): EmitAny(StackOffset(ParamVar1))     ' mov eax, dword ptr [esp + StackOffset(ParamVar1)]   (reads input param into EAX)
        Emit(&H89, &H84, &H24): EmitAny(StackOffset(Foo))           ' mov dword ptr [esp + StackOffset(Foo)], eax   (writes it to our local variable, ABC)
        MsgBox Foo          ' Foo has been populated with the passed in Something1 value
    End Sub
    Public Sub TestLocalVar2()
        TestLocalVar2a(555)
    End Sub
WaynePhillipsEA commented 3 days ago

Note that whilst X64 calling convention stipulates that the first 4 arguments are passed in registers (i.e. not on the stack), you shouldn't have to worry about this, as the tB prolog that sets up the stack frame moves the values passed via-registers onto the stack anyway. (Code using Emit() will not be compiled via LLVM, and so this side effect of the tB prolog can be relied upon here).

WaynePhillipsEA commented 3 days ago

Tip: if using Emit() like shown above (ie. as part of a regular tB procedure code), then assume that only volatile registers are available for your use (i.e. EAX/ECX/EDX on x86). If you clobber any other registers, ensure you preserve and restore them so that you don't interfere with any other tB codegen that may be relying upon their values. If in doubt, ask :)