calling an external C program from QB64 & getting back results? - madscijr - 09-07-2022
Some background:
I'm working on some C code to read separate input from 2 or more USB mice plugged into the PC. That will be its own EXE file (unless it should be a DLL or something?), and when called, it looks for command line parameters. If no command line param is sent, it just returns the count of how many mice are connected to the system Else the command line parameter contains the index of the mouse to return input from, and it returns 2 numbers dx and dy, maybe just the two numbers separated by a comma, e.g. "{dx},{dy}".
I know the SHELL command can be used to call an external EXE from QB64, and theoretically you should be able to pipe the output of the EXE to a file, and read that from QB64. This is a very rudimentary method to get the two programs talking, and it seems like a very inefficient way to do it. Moreover, it doesn't seem to be working - the SHELL command in the test program doesn't seem to be redirecting the output correctly to a file, I am seeing a file not found error.
Would anyone have any ideas about a better way to do this, or even why the SHELL command isn't piping the output to a file?
Below is my QB64 program, followed by the external C program it is calling, which can be compiled to EXE using QB64 using included the batch file. (The attached ZIP has the precompiled EXEs and the source.)
Any help appreciated!
The main program "my_qb64_program.bas":
Code: (Select All) ' CALL AN EXTERNAL PROGRAM AND GET BACK SOME RESULTS
' IS SHELL AND REDIRECTING OUTPUT TO A FILE THE BEST WAY
' OR IS THERE A MORE DIRECT METHOD?
_Title "Talk to EXE from QB64"
Const FALSE = 0
Const TRUE = Not FALSE
Dim Shared m_ProgramPath$: m_ProgramPath$ = Left$(Command$(0), _InStrRev(Command$(0), "\"))
Dim sExePath As String
Dim sOutPath As String
Dim sParams As String
Dim sCommand As String
Dim sResult As String
' BUILD COMMAND LINE FOR SHELL, DIRECT OUTPUT TO FILE sOutPath
sExePath = m_ProgramPath$ + "my_c_program.exe"
sOutPath = m_ProgramPath$ + "my_c_program_output.txt"
sParams = "1 2 3"
'sCommand = Chr$(34) + sExePath + Chr$(34) + " " + sParams + " > " + Chr$(34) + sOutPath + Chr$(34)
sCommand = Chr$(34) + sExePath + " " + sParams + Chr$(34) + " > " + Chr$(34) + sOutPath + Chr$(34)
' CALL THE EXE WITH SOME COMMAND LINE PARAMETERS
Cls
Print "Shell _Hide " + sCommand: Print
Shell _Hide sCommand
' RETRIEVE THE OUTPUT <- IS THERE A MORE EFFICIENT WAY THAN USING A FILE?
Print "Output should be in file:"
Print Chr$(34) + sOutPath + Chr$(34): Print
sResult = ReadFile$(sOutPath, "(file not found)")
' SHOW RESULTS
Print "Contents of output file:"
Print Chr$(34) + sResult + Chr$(34): Print
' DONE
End
' /////////////////////////////////////////////////////////////////////////////
' Fastest way is always to just read the whole life at once and then parse it.
Function ReadFile$ (sFileName As String, sDefault As String)
Dim x$
If _FileExists(sFileName) Then
Open sFileName For Binary As #1
x$ = Space$(LOF(1))
Get #1, 1, x$
Close #1
ReadFile$ = x$
Else
ReadFile$ = sDefault
End If
End Function ' ReadFile$
The second program, "my_c_program.c":
Code: (Select All) /******************************************************************************
Test to see how QB64 can call a C program with some command line arguments,
and get back some results.
******************************************************************************/
#include <stdio.h>
#include <string.h>
int main (int argc, char* argv[])
{
printf("%s", "result:");
for (int x=1; x < argc; ++x)
{
if (x > 1)
{
printf("%s", ",");
}
printf("%s", argv[x]);
}
return 0;
} // main
The batch file "COMPILE_C_PROG.BAT" that will compile the above C code for you using QB64's built in C compiler
(edit line 13 to point to the QB64 directory on your PC):
Code: (Select All) @echo off
:: %QB64DIR% = C:\Users\maduser\Documents\Code\qb64
:: %MGWDIR% = C:\Users\maduser\Documents\Code\qb64\internal\c\c_compiler
:: %PATH% = C:\Users\maduser\Documents\Code\qb64\internal\c\c_compiler\bin
:: %LIBRARY_PATH% = C:\Users\maduser\Documents\Code\qb64\internal\c\c_compiler\x86_64-w64-mingw32\lib
:: %CPATH% = C:\Users\maduser\Documents\Code\qb64\internal\c\c_compiler\x86_64-w64-mingw32\include
:: PUT THE NAME OF YOUR PROGRAM TO COMPILE HERE
SET PROGNAME=my_c_program
:: QB64DIR MUST POINT TO YOUR QB64 DIRECTORY, LIKE THIS: SET QB64DIR=C:\PROG\QB64
SET QB64DIR=C:\Users\maduser\Documents\Code\qb64
if not "%QB64DIR%"=="" goto doit
ECHO.
ECHO.
ECHO Edit line 4 of this batch file and set QB64DIR to point to your QB64 directory!
ECHO.
ECHO.
GOTO lunch
:doit
:: set up environment vars for direct invocation of QB64's included MinGW C/C++ compiler....
:: WE'LL SKIP THE SETUP IF WE'VE BEEN THROUGH THIS BEFORE....
:: WE'LL USE THE PRESENCE OR ABSENCE OF MGWDIR TO TELL US IF WE'VE PREVIOUSLY SET THE ENVARS.
:: IF MGWDIR ALREADY EXISTS, THEN SKIP THE SETUP SO WE DON'T KEEP ADDING THE SAME
:: STUFF TO THE PATH ENVAR OVER AND OVER EVERY TIME WE RUN THIS BATCH FILE....
if not "%MGWDIR%"=="" goto work
:: SET MGWDIR TO POINT TO MINGW IN OUR QB64 INSTALLATION DIRECTORY.
:: (THIS BATCH FILE SHOULD BE IN THE MAIN QB64 DIRECTORY)....
set MGWDIR=%QB64DIR%\internal\c\c_compiler
set PATH=%MGWDIR%\bin;%PATH%
set LIBRARY_PATH=%MGWDIR%\x86_64-w64-mingw32\lib
set CPATH=%MGWDIR%\x86_64-w64-mingw32\include
:work
:: NOTE: THE LINE BELOW IS SET TO PRODUCE A 32-BIT EXECUTABLE.
:: REPLACE -m32 WITH -m64 TO GENERATE 64-BIT EXEs
:: (OR REMOVE THE -m OPTION ENTIRELY TO GENERATE THE COMPILER DEFAULT)....
gcc -Wall -Os -s -m32 --static -o "%~dp1%PROGNAME%.exe" "%~dp1%PROGNAME%.c"
:lunch
pause
RE: calling an external C program from QB64 & getting back results? - SpriggsySpriggs - 09-07-2022
Hey! Here is the code I wrote for reading the stdout and stderr of a program for Mac, Linux, and Windows! No files and no command prompt window! Enjoy
Pipecom
RE: calling an external C program from QB64 & getting back results? - madscijr - 09-07-2022
(09-07-2022, 06:41 PM)Spriggsy Wrote: Hey! Here is the code I wrote for reading the stdout and stderr of a program for Mac, Linux, and Windows! No files and no command prompt window! Enjoy
Pipecom
Thanks Spriggsy, that sounds like just what the doctor ordered.
I'm just about out of time for today - will check back soon and let you know how that's working...
RE: calling an external C program from QB64 & getting back results? - SpriggsySpriggs - 09-07-2022
(09-07-2022, 06:53 PM)madscijr Wrote: Thanks Spriggsy, that sounds like just what the doctor ordered.
I'm just about out of time for today - will check back soon and let you know how that's working...
Oh, I actually forgot that the Mac and Linux versions both use a file for stderr (but not for stdout). However, you'll most likely be using the Windows portion anyways.
RE: calling an external C program from QB64 & getting back results? - madscijr - 09-07-2022
(09-07-2022, 07:00 PM)Spriggsy Wrote: (09-07-2022, 06:53 PM)madscijr Wrote: Thanks Spriggsy, that sounds like just what the doctor ordered.
I'm just about out of time for today - will check back soon and let you know how that's working...
Oh, I actually forgot that the Mac and Linux versions both use a file for stderr (but not for stdout). However, you'll most likely be using the Windows portion anyways.
Yes, Windows. Eventually it would be nice to get this mouse thing working cross-platform, but that's for another day / month / year.
I couldn't resist trying it before I go, and it works! Thank you!
Code: (Select All) _Title "Talk to EXE" ' display in the Window's title bar
' LOCAL VARIABLES
Dim sExePath As String
Dim sParams As String
Dim sCommand As String
Dim sResult As String
' CALL A PROGRAM AND GET BACK SOME RESULTS
sExePath = m_ProgramPath$ + "args_v2.exe"
sParams = "1 2 3"
sCommand = Chr$(34) + sExePath + Chr$(34) + " " + sParams
Cls
Print sCommand
Print
sResult = pipecom_lite$(sCommand)
Print "Contents of output:"
Print Chr$(34) + sResult + Chr$(34)
Print
End
' /////////////////////////////////////////////////////////////////////////////
' Spriggsys-API-Collection
' https://github.com/SpriggsySpriggs/Spriggsys-API-Collection/blob/master/Cross-Platform%20(Windows%2C%20Macintosh%2C%20Linux)/pipecomqb64.bas
Function pipecom& (cmd As String, stdout As String, stderr As String)
$If WIN Then
Type SECURITY_ATTRIBUTES
nLength As Long
$If 64BIT Then
padding As Long
$End If
lpSecurityDescriptor As _Offset
bInheritHandle As Long
$If 64BIT Then
padding2 As Long
$End If
End Type
Type STARTUPINFO
cb As Long
$If 64BIT Then
padding As Long
$End If
lpReserved As _Offset
lpDesktop As _Offset
lpTitle As _Offset
dwX As Long
dwY As Long
dwXSize As Long
dwYSize As Long
dwXCountChars As Long
dwYCountChars As Long
dwFillAttribute As Long
dwFlags As Long
wShowWindow As Integer
cbReserved2 As Integer
$If 64BIT Then
padding2 As Long
$End If
lpReserved2 As _Offset
hStdInput As _Offset
hStdOutput As _Offset
hStdError As _Offset
End Type
Type PROCESS_INFORMATION
hProcess As _Offset
hThread As _Offset
dwProcessId As Long
$If 64BIT Then
padding2 As Long
$End If
End Type
Const STARTF_USESTDHANDLES = &H00000100
Const CREATE_NO_WINDOW = &H8000000
Const INFINITE = 4294967295
Const WAIT_FAILED = &HFFFFFFFF
Declare Dynamic Library "Kernel32"
Function CreatePipe% (ByVal hReadPipe As _Offset, Byval hWritePipe As _Offset, Byval lpPipeAttributes As _Offset, Byval nSize As Long)
Function CreateProcess% Alias CreateProcessA (ByVal lpApplicationName As _Offset, Byval lpCommandLine As _Offset, Byval lpProcessAttributes As _Offset, Byval lpThreadAttributes As _Offset, Byval bInheritHandles As Integer, Byval dwCreationFlags As Long, Byval lpEnvironment As _Offset, Byval lpCurrentDirectory As _Offset, Byval lpStartupInfor As _Offset, Byval lpProcessInformation As _Offset)
Function CloseHandle% (ByVal hObject As _Offset)
Function ReadFile% (ByVal hFile As _Offset, Byval lpBuffer As _Offset, Byval nNumberOfBytesToRead As Long, Byval lpNumberOfBytesRead As _Offset, Byval lpOverlapped As _Offset)
Function GetExitCodeProcess% (ByVal hProcess As _Offset, Byval lpExitCode As _Offset)
Function WaitForSingleObject& (ByVal hHandle As _Offset, Byval dwMilliseconds As Long)
End Declare
Dim As Integer ok: ok = 1
Dim As _Offset hStdOutPipeRead
Dim As _Offset hStdOutPipeWrite
Dim As _Offset hStdReadPipeError
Dim As _Offset hStdOutPipeError
Dim As SECURITY_ATTRIBUTES sa: sa.nLength = Len(sa): sa.lpSecurityDescriptor = 0: sa.bInheritHandle = 1
If CreatePipe(_Offset(hStdOutPipeRead), _Offset(hStdOutPipeWrite), _Offset(sa), 0) = 0 Then
pipecom = -1
Exit Function
End If
If CreatePipe(_Offset(hStdReadPipeError), _Offset(hStdOutPipeError), _Offset(sa), 0) = 0 Then
pipecom = -1
Exit Function
End If
Dim As STARTUPINFO si
si.cb = Len(si)
si.dwFlags = STARTF_USESTDHANDLES
si.hStdError = hStdOutPipeError
si.hStdOutput = hStdOutPipeWrite
si.hStdInput = 0
Dim As PROCESS_INFORMATION pi
Dim As _Offset lpApplicationName
Dim As String fullcmd: fullcmd = "cmd /c " + cmd + Chr$(0)
Dim As String lpCommandLine: lpCommandLine = fullcmd
Dim As _Offset lpProcessAttributes
Dim As _Offset lpThreadAttributes
Dim As Integer bInheritHandles: bInheritHandles = 1
Dim As Long dwCreationFlags: dwCreationFlags = CREATE_NO_WINDOW
Dim As _Offset lpEnvironment
Dim As _Offset lpCurrentDirectory
ok = CreateProcess(lpApplicationName,_
_Offset(lpCommandLine),_
lpProcessAttributes,_
lpThreadAttributes,_
bInheritHandles,_
dwCreationFlags,_
lpEnvironment,_
lpCurrentDirectory,_
_Offset(si),_
_Offset(pi))
If ok = 0 Then
pipecom = -1
Exit Function
End If
ok = CloseHandle(hStdOutPipeWrite)
ok = CloseHandle(hStdOutPipeError)
Dim As String buf: buf = Space$(4096 + 1)
Dim As Long dwRead
While ReadFile(hStdOutPipeRead, _Offset(buf), 4096, _Offset(dwRead), 0) <> 0 And dwRead > 0
buf = Mid$(buf, 1, dwRead)
GoSub RemoveChr13
stdout = stdout + buf
buf = Space$(4096 + 1)
Wend
While ReadFile(hStdReadPipeError, _Offset(buf), 4096, _Offset(dwRead), 0) <> 0 And dwRead > 0
buf = Mid$(buf, 1, dwRead)
GoSub RemoveChr13
stderr = stderr + buf
buf = Space$(4096 + 1)
Wend
Dim As Long exit_code
Dim As Long ex_stat
If WaitForSingleObject(pi.hProcess, INFINITE) <> WAIT_FAILED Then
If GetExitCodeProcess(pi.hProcess, _Offset(exit_code)) Then
ex_stat = 1
End If
End If
ok = CloseHandle(hStdOutPipeRead)
ok = CloseHandle(hStdReadPipeError)
If ex_stat = 1 Then
pipecom = exit_code
Else
pipecom = -1
End If
Exit Function
RemoveChr13:
Dim j As Long
j = InStr(buf, Chr$(13))
Do While j
buf = Left$(buf, j - 1) + Mid$(buf, j + 1)
j = InStr(buf, Chr$(13))
Loop
Return
$Else
Declare CustomType Library
Function popen%& (cmd As String, readtype As String)
Function feof& (ByVal stream As _Offset)
Function fgets$ (str As String, Byval n As Long, Byval stream As _Offset)
Function pclose& (ByVal stream As _Offset)
Function fclose& (ByVal stream As _Offset)
End Declare
Declare Library
Function WEXITSTATUS& (ByVal stat_val As Long)
End Declare
Dim As String pipecom_buffer
Dim As _Offset stream
Dim buffer As String * 4096
If _FileExists("pipestderr") Then
Kill "pipestderr"
End If
stream = popen(cmd + " 2>pipestderr", "r")
If stream Then
While feof(stream) = 0
If fgets(buffer, 4096, stream) <> "" And feof(stream) = 0 Then
stdout = stdout + Mid$(buffer, 1, InStr(buffer, Chr$(0)) - 1)
End If
Wend
Dim As Long status
Dim As Long exit_code
status = pclose(stream)
exit_code = WEXITSTATUS(status)
If _FileExists("pipestderr") Then
Dim As Integer errfile
errfile = FreeFile
Open "pipestderr" For Binary As #errfile
If LOF(1) > 0 Then
stderr = Space$(LOF(1))
Get #errfile, , stderr
End If
Close #errfile
Kill "pipestderr"
End If
pipecom = exit_code
Else
pipecom = -1
End If
$End If
End Function ' pipecom&
Function pipecom_lite$ (cmd As String)
Dim As Long a
Dim As String stdout, stderr
a = pipecom(cmd, stdout, stderr)
pipecom_lite$ = stdout
End Function ' pipecom_lite$
RE: calling an external C program from QB64 & getting back results? - mnrvovrfc - 09-08-2022
(09-07-2022, 07:00 PM)Spriggsy Wrote: Oh, I actually forgot that the Mac and Linux versions both use a file for stderr (but not for stdout). However, you'll most likely be using the Windows portion anyways. The "pipecom_lite$()" should have been changed to have an operating system conditional to decide to return "stdout" (Windows) or "stderr" (Unix-based). Otherwise, thank you very much for this tool.
The "io.popen()" in Lua does pretty much the same thing, makes unnecessary needing a file to have a "DIR /B/S" on Windows or "ls -1ar" or alike on Linux, only to have a cheap string list of files.
RE: calling an external C program from QB64 & getting back results? - SpriggsySpriggs - 09-08-2022
@mnrvovrfc
Glad you like the little library. Thanks for your comment.
|