ProgEx17 - C++ Dll With PB Win Host And Explicit Linking; Output To Console

Started by Frederick J. Harris, November 07, 2009, 03:19:03 AM

Previous topic - Next topic

0 Members and 1 Guest are viewing this topic.

Frederick J. Harris

First, C++ Dll


/*
  ProgEx17
 
  Create a new C++ project named ProgEx17.  Make it a Windows Dll project.  Add
  this file to the project after removing any files your development environment
  may have added for you.  In these tutorials we create our own projects from
  scratch, and we write all our own code!
 
  The below code will use the GetStdHandle() Api function to obtain a handle
  to standard output, and the WriteConsole() function to write a character
  string to there (your display/monitor/screen).   We've seen to it through the
  prototype/declaration that the procedure will be exported from this Dll as
  Print.  This won't in any way interfere with our calling this procedure
  through its address using PowerBASIC's Call Dword syntax in a host program.
  At this point my discussion will continue in PBWinHost.bas......... 
*/

#include <windows.h>
extern "C" __declspec(dllexport) void Print(char*); //Exported Function Declare

void Print(char* szMsg)
{
HANDLE hStdOut;
DWORD iLen;

hStdOut=GetStdHandle(STD_OUTPUT_HANDLE);
WriteConsole(hStdOut,szMsg,strlen(szMsg),&iLen,NULL);
}


Now The PowerBASIC Windows Host


#Compile Exe
'PBWinHost.bas
'
'.....continued from ProgEx17
'
'We're going to do something a little different here.  Instead of doing the
'easy thing and using the PowerBASIC Console Compiler, we'll use PowerBASIC
'Windows.  I'm using PBWin 9 but this should work with any of the earlier
'PowerBASIC Windows compilers.  Since our Dll, i.e., ProgEx17.dll created
'in C++ needs a console screen to output to, we need to use the AllocConsole()
'Api function to create a console.  Yes, you can do this in PowerBASIC Windows.
'It really couldn't be much easier.  The first function call in PBMain() calls
'AllocConsole() and you have a console window.  Its as easy as that!
'
'The next statement is a call to the LoadLibrary() Api function.  You should
'have created ProgEx17.dll in whatever development environment you are using,
'e.g., Dev-C++, CodeBlocks, Visual Studio # whatever, and this .Bas program,
'i.e., PBWinHost.bas should create its executable in that same directory so it
'can locate the dll it is calling.
'
'Next we call GetStdHandle() to obtain a handle to the input buffer of the
'console window we just created.  We'll need that to test for keystrokes.  We
'are only looking for an '...any key to continue...' sort of thing, because all
'we want to do is hold the console window open so we can see output.  Otherwise
'it would flash open and immediately disappear after displaying whatever output
'we want to display.  Towards the end of PBMain() you see my WaitKey() approx-
'imation to accomplish that task.  If you examine my Waitkey() procedure and
'are at all conversant in the topic of Window's Processes, you might think I'm
'burning processor cycles in the Do Loop, but in point of fact ReadConsoleInput
'waits in a low consumption 'Wait State' for input.  It doesn't itself 'poll'.
'
'If LoadLibrary() succeeds we enter the inside of the 'If' statement and then
'obtain another console handle but this time to our console's output buffer.
'That handle will allow us to write to the output buffer, and what gets written
'there becomes visible, i.e., it is displayed/printed.
'
'Our next step is the interesting part and we call GetProcAddress() just like
'we did in the past two C++ programs but look how much easier the call is.  We
'simply assign the return address from GetProcAddress() to our Dword variable
''pFn'.  What could be easier?  No casts, no confusing typedefs, nothing!  Just
'a simple assignment!  We next test to see if the function call succeeded.  If
'GetProcAddress() succedes pFn will contain a non-null value.  You always need
'to test this sort of thing because if you don't and GetProcAddress() fails
'you've got an instant crash due to calling a null pointer.
'
'If GetProcAddress() succeedes we get to use my 'homemade' Locate function which
'uses the SetConsoleCursorPosition() Api function to anchor the cursor at your
'chosen position on the screen.  As with my other examples I'm not wanting to be
'very eloquent or original - I just want to output 'Hello, World!'; but I do
'want it centered more or less on the screen.  You should be able to figure out
'the logic used.  Or maybe not.  There is a twist to it.  The actual function
'uses a COORD C struct which is an amalgamation of two C short ints.  Power-
'BASIC integers are an exact match.  And we fudge the whole thing by combining
'the two 16 bit integers using thr MakDwd macro which gives us something the
'C function won't be able to tell apart from a C COORD struct!  This nonsense
'occurs because the PowerBASIC Declare was created before PowerBASIC had the
'ability to pass UDTs (Type Variables), and for backwards compatibility the
'Declare was maintained in this archaic state.  If you wanted to write a
'different Declare using a real COORD TYPE you could.
'
'Anyway, now for the important part!  The part we've been building up to!  We
'have the address of the Prnt procedure loaded into our process through the Dll
'and all we need to do is call it. Remember in C/C++ we had to very carefully
'specify to the compiler that the Prnt function we wanted to call did not have
'a return value, i.e., it returned void, and it took just one character pointer
'parameter, i.e., a char*?  Well, if we want to call this address & function in
'PowerBASIC we have to specify to PowerBASIC the exact same thing.  But its not
'nearly so obtuse and obscure in PowerBASIC.  With PowerBASIc we use the Call
'Dword statement, and instead of needing any confusing 'casts' as in C/C++, we
'just tell PowerBASIC what the function looks like in terms of return values,
'calling conventions if needed, and parameters.  We do this with a model proto-
'type or Declare.  Right under the Include is this line...
'
'Declare Sub Prnt(Byref szMsg As Asciiz)
'
'There really is no 'Prnt' Sub in this program.  All this declaration does is
'provide a template for PowerBASIC to use so that it can set up a function call
'at any address for any function that doesn't return anything and takes a char*
'as a parameter.  So what...
'
'Call Dword pFn Using Prnt("Hello, World!")
'
'does is set up the stack for a function call at whatever address is contained
'in pFn using the exact function signature of 'Prnt'.  Run the program and
'you'll see it works!
'
'And for no extra charge I've provided a 'Cls' for you!

#Dim All
#Include "Win32Api.inc"
Declare Sub Prnt CDecl (Byref szMsg As Asciiz)

Sub Cls(hStdOut As Dword)
  Local csbi As CONSOLE_SCREEN_BUFFER_INFO
  Local dwConsoleSize As Dword
  Local dwWritten As Dword
  Local dwXY As Dword

  Call GetConsoleScreenBufferInfo(hStdOut,csbi)
  dwConsoleSize=csbi.dwSize.X * csbi.dwSize.Y
  Call FillConsoleOutputCharacter(hStdOut,32,dwConsoleSize,dwXY,dwWritten)
  Call GetConsoleScreenBufferInfo(hStdOut,csbi)
  Call FillConsoleOutputAttribute(hStdOut,csbi.wAttributes,dwConsoleSize,dwXY,dwWritten)
  Call SetConsoleCursorPosition(hStdOut,dwXY)
End Sub


Sub Locate(hStdOutput As Dword, x As Integer, y As Integer)
  Local dwXY As Dword

  dwXY=MakDwd(x,y)
  Call SetConsoleCursorPosition(hStdOutput,dwXY)
End Sub


Sub Waitkey(hStdInput As Dword)
  Local ir As INPUT_RECORD
  Local blnContinue As Long
  Local dwInputEvents As Dword

  blnContinue=%TRUE
  Do While blnContinue=%TRUE
     Call ReadConsoleInput(hStdInput,ir,1,dwInputEvents)
     If ir.EventType=%KEY_EVENT Then
        blnContinue=%FALSE
     End If
  Loop
End Sub


Function PBMain () As Long
  Local hStdInput,hStdOutput,hIns,pFn As Dword

  Call AllocConsole()
  hIns=LoadLibrary("ProgEx17.dll")
  hStdInput=GetStdHandle(%STD_INPUT_HANDLE)
  If hIns Then
     hStdOutput=GetStdHandle(%STD_OUTPUT_HANDLE)
     pFn=GetProcAddress(hIns,"Print")
     If pFn Then
        Locate(hStdOutput,33,9)
        Call Dword pFn Using Prnt("Hello, World!")
     End If
  End If
  Waitkey(hStdInput)

  PBMain=0
End Function

Frederick J. Harris

Needed to specify CDecl on this one too.  I see it won't compile with PB 10 (must have been done with PB 8 or 9).  So I need to fix that also.  Will get to it shortly.

Frederick J. Harris

The above program needs to be modified a bit for PowerBASIC 10.02 because the declare for ReadConsoleInput was changed between PB9 and PB10 as so...


Declare Function ReadConsoleInput Lib "KERNEL32.DLL" Alias "ReadConsoleInputA" _      'PowerBASIC 9
( _
  Byval hConsoleInput As Dword, _
  Byref lpBuffer As Any, _
  BYVAL nNumberOfCharsToRead As Long, _
  Byref lpNumberOfCharsRead As Long _
) As Long

Declare Function ReadConsoleInputA Lib "KERNEL32.DLL" Alias "ReadConsoleInputA"       'PowerBASIC 10
(
Byval hConsoleInput As Dword, _
Byval lpBuffer As INPUT_RECORD PTR,
Byval nNumberOfCharsToRead As Long, _
Byref lpNumberOfCharsRead As Long
)As Long


The 2nd parameter is now Byval instead of Byref, so if you declare an INPUT_RECORD as a local, you'll need to pass its address to the WinApi function, because its expecting a pointer.  Here is the modified program...


#Compile Exe 'Compiles with PowerBASIC Windows 10.02
#Dim All
#Include "Win32Api.inc"
Declare Sub Prnt CDecl Lib "ProgEx17.dll" Alias "Print" (Byref szMsg As Asciiz)

Sub Cls(hStdOut As Dword)
  Local csbi As CONSOLE_SCREEN_BUFFER_INFO
  Local dwConsoleSize As Dword
  Local dwWritten As Dword
  Local dwXY As Dword

  Call GetConsoleScreenBufferInfo(hStdOut,csbi)
  dwConsoleSize=csbi.dwSize.X * csbi.dwSize.Y
  Call FillConsoleOutputCharacter(hStdOut,32,dwConsoleSize,dwXY,dwWritten)
  Call GetConsoleScreenBufferInfo(hStdOut,csbi)
  Call FillConsoleOutputAttribute(hStdOut,csbi.wAttributes,dwConsoleSize,dwXY,dwWritten)
  Call SetConsoleCursorPosition(hStdOut,dwXY)
End Sub


Sub Waitkey(hStdInput As Dword)
  Local ir As INPUT_RECORD
  Local blnContinue As Long
  Local dwInputEvents As Dword

  blnContinue=%TRUE
  Do While blnContinue=%TRUE
     Call ReadConsoleInput(hStdInput,Varptr(ir),1,dwInputEvents)     'Declare now Byval Varptr(ir)
     If ir.EventType=%KEY_EVENT Then                                 'instead of Byref ir
        blnContinue=%FALSE
     End If
  Loop
End Sub


Function PBMain () As Long
  Local dwXY,hStdInput,hStdOutput,hIns As Dword

  hIns=LoadLibrary("ProgEx17.dll")
  If hIns Then
     Call AllocConsole()
     hStdOutput=GetStdHandle(%STD_OUTPUT_HANDLE)
     dwXY=MakDwd(33,9)
     Call SetConsoleCursorPosition(hStdOutput,dwXY)
     Call Prnt("Hello, World!")
  End If
  hStdInput=GetStdHandle(%STD_INPUT_HANDLE)
  Waitkey(hStdInput)

  PBMain=0
End Function         

Frederick J. Harris

It might be interesting to note that in the C code from above reproduced below the function Print is specified without a calling convention, so it defaults to __cdecl...


#include <windows.h>
extern "C" __declspec(dllexport) void Print(char*); //Exported Function Declare

void Print(char* szMsg)
{
HANDLE hStdOut;
DWORD iLen;

hStdOut=GetStdHandle(STD_OUTPUT_HANDLE);
WriteConsole(hStdOut,szMsg,strlen(szMsg),&iLen,NULL);
}


Therefore, the PowerBASIC declare needs to be...


Declare Sub Prnt CDecl Lib "ProgEx17.dll" Alias "Print" (Byref szMsg As Asciiz)


Note further that the C Print function is a wrapper on WriteConsole which is specified as so in the Windows header WinCon.h...


BOOL WINAPI WriteConsoleA
(
HANDLE       hConsoleOutput,
CONST VOID   *lpBuffer,
DWORD        nNumberOfCharsToWrite,
LPDWORD      lpNumberOfCharsWritten,
LPVOID       lpReserved
);


The 'WINAPI' token is a #define for __stdcall, which explains why we don't need to specify calling conventions for most Microsoft specific Windows functions we use in PowerBASIC, since __stdcall, i.e., SDECL is standard there.  Of course, this is the case in COM Apis also.