QB64 Phoenix Edition
calling an external C program from QB64 & getting back results? - Printable Version

+- QB64 Phoenix Edition (https://staging.qb64phoenix.com)
+-- Forum: QB64 Rising (https://staging.qb64phoenix.com/forumdisplay.php?fid=1)
+--- Forum: Code and Stuff (https://staging.qb64phoenix.com/forumdisplay.php?fid=3)
+---- Forum: Help Me! (https://staging.qb64phoenix.com/forumdisplay.php?fid=10)
+---- Thread: calling an external C program from QB64 & getting back results? (/showthread.php?tid=866)



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.