C++ API
The C++ API has 2 ways of executing your program:
- Using the
execandwait
function - Using the
ScriptRunner
class
The execandwait
function
The execandwait
function under the uexec
namespace takes a null-terminated array of CLI arguments like this:
char* const arr[] = { "test.exe", nullptr };
auto result = uexec::execandwait(arr);
The function will find the test.exe
executable in the
system's path, and will run execute it as a background process.
The return value is an integer, if equal to -1, there is an error, else it returns 0 on success.
The ScriptRunner
class
The ScriptRunner
class has the following members:
class ScriptRunner
{
public:
// Given an array, will start executing the script, will return -1 on failure
int init(char* const* args, bool bOpenStderrPipe = false, bool bOpenStdoutPipe = false, bool bOpenStdinPipe = false) noexcept;
// Updates the buffer stream, call this with true the first time
void update() noexcept;
// Destroys the runner
void destroy() noexcept;
void destroyForReuse() noexcept;
[[nodiscard]] bool valid() const noexcept;
[[nodiscard]] bool finished() const noexcept;
[[nodiscard]] bool startable() const noexcept;
// Terminates the process, use the destroy functions after calling terminate!
void terminate() noexcept;
// Read from STDOUT, STDIN and STDERR
bool readSTDOUT(uexecstring& buffer, size_t size, size_t& bytesRead) noexcept;
bool readSTDERR(uexecstring& buffer, size_t size, size_t& bytesRead) noexcept;
// Write to STDOUT, STDIN and STDERR
bool write(uexecstring& buffer, size_t size, size_t& bytesWritten) noexcept;
};
The member functions work like this:
- The
init
function takes the same arguments asexecandwait
, but it has additional arguments. These additional arguments are default booleans that toggle different pipes on(STDIN, STDOUT, STDERR). Theinit
function returns the same result asexecandwait
. - The
update
function updates the runner. Run this at any point you want. This simply checks if the child is still alive. It's important to call this quite frequently. - The
destroy
function simply destroys the runner, but does not reinitialise it. - The
destroyForReuse
function destroys the runner and reinitialises it. - The
valid
function returns a bool that you can use to check it the runner is valid. - The
finished
function returns a boolean, that you can use to check, whether the program ended its execution. - On the contrary, the
startable
function returns whether you can start a new process from the runner. - The
terminate
function simply terminates the process at any point of its execution.
Reading and writing from and to streams
You can easily read and write to the child's streams by using the
read
and write
member functions. They look
like this
bool readSTDOUT(uexecstring& buffer, size_t size, size_t& bytesRead) noexcept;
bool readSTDERR(uexecstring& buffer, size_t size, size_t& bytesRead) noexcept;
bool write(UExecStreams stream, uexecstring& buffer, size_t size, size_t& bytesWritten) noexcept;
The first 2 functions simply write to the child process' STDERR or STDOUT. The 3rd function writes to the child's STDIN. For these to function, you must have initialised the runner with the needed pipes.
C API
There are 2 ways to use the C API:
- Use it like the C++ one, by compiling the library and using the header file.
- To compile as a C library:
- Compile all files including the ones under the
C
folder. - Next, include the
C/cuexec.h
and start using the API.
- Compile all files including the ones under the
The functions in the header look like this:
int uexec_execandwait(char* const* command);
// Given an array, will start executing the script, will return -1 on failure
int uexec_runner_init(RunnerData* data, char* const* args, bool bOpenStderrPipe, bool bOpenStdoutPipe, bool bOpenStdinPipe);
// Updates the buffer stream, call this with true the first time
void uexec_runner_update(RunnerData* data);
// Destroys the runner
void uexec_runner_destroy(RunnerData* data);
bool uexec_runner_valid(RunnerData* data);
// Makes the runner reusable
void uexec_runner_destroyForReuse(RunnerData* data);
bool uexec_runner_finished(RunnerData* data);
bool uexec_runner_startable(RunnerData* data);
// Terminates the process, use the destroy functions after calling terminate!
void uexec_runner_terminate(RunnerData* data);
// Read from STDOUT, STDIN and STDERR
bool uexec_runner_readSTDOUT(RunnerData* data, char* buffer, size_t size, size_t* bytesRead);
bool uexec_runner_readSTDERR(RunnerData* data, char* buffer, size_t size, size_t* bytesRead);
// Write to STDOUT, STDIN and STDERR
bool uexec_runner_write(RunnerData* data, const char* buffer, size_t size, size_t* bytesWritten);
As you can see, the C API is the same as the C++ one, but instead of using a class, we use functions that take a data struct that contains the data used by the class. This is because the C API is just a simple wrapper.
The other way to use it is to forgo header files and simply load
functions dynamically at runtime, using functions like
dlsym
. MadLadSquad
offers a cross-platform library that offers this functionality, the UntitledRuntimeLibraryLoader.
Examples
Here is an example for the C++ API:
void execfunc()
{
char* const args[] = { (char*)"https://madladsquad.com/testingdt", nullptr };
::ScriptRunner runner{};
uexec.init(args, false, true, false);
runner.update();
runner
std::string buff;
.resize(256);
buffsize_t bytes = 0;
std::this_thread::sleep_for(std::chrono::seconds(2));
.readSTDOUT(buff, buff.size(), bytes);
runner.erase(bytes);
buffstd::cout << bytes << std::endl;
std::cout << buff << std::endl;
}
Here is an example for the C API:
void execfunc()
{
char* const args[] = { (char*)"https://madladsquad.com/testingdt", nullptr };
{};
RunnerData data(&data, args, false, true, false);
uexec_runner_init(&data);
uexec_runner_update
std::string buff;
.resize(256);
buffsize_t bytes = 0;
std::this_thread::sleep_for(std::chrono::seconds(2));
(&data, buff.data(), buff.size(), &bytes);
uexec_runner_readSTDOUT.erase(bytes);
buffstd::cout << bytes << std::endl;
std::cout << buff << std::endl;
}
While this is written in C++, it's still using the C API. There will be no difference if you replaced the calls to the C++ standard library with the C standard library.
Handling process termination and reusing runners
In this examplem, we'll use the C++ API, but as you have seen, the C API uses basically the same function names, so it shouldn't be an issue to port to C.
One issue you might often face is handling of process termination and reusing runners. Generally the library is pretty ambiguous about that type of functionality so here, we'll detail it.
When the child exits there are 2 scenarios:
- You explicitly terminated a process by calling the
terminate()
function - The application finished by itself.
As said here, to terminate a process we use the
terminate()
function. If an application exits by itself you
generally don't have to do anything except check for its state. It's
generally a good idea to do state checks on the process whenever you
can.
State checks are simple, simply query the finished()
function at a good interval. If the function returned true
then the process is still running. If false
call the
destroy
or destroyForReuse
functions.
The destroy
function can be called even if the process
hasn't ended execution. In this case, it calls terminate()
.
The function basically puts the class in an unusable state. To check if
it's in this state, you can use the valid()
function. If it
is valid, and you want to reuse the process, call
destroyForReuse()
. It will reinitialise the class and will
mark it as a valid one. After that, you can call init
again
The destroyForReuse
function as said before,
re-initialises the process so that it can be reused. Under the hood, it
checks if the process has finished execution, if it hasn't, it calls
destroy
and then re-initialises everything in the
class.
Since destroyForReuse
resets all flags to be valid
instead of checking for validity, you can use the
startable()
function, which can also be used to
differentiate between newly created runners and reused runners, since
the init
function resets the startable
flag
internally but destroyForReuse
enables it.
Unix notes and possible unexpected behaviour
Unmanaged child processes stop execution whenever a function from the UntitledExec library is called
If you know how Unix operates, you know that the job of this library
is to run fork
and exec
. Well, if you have
ever wondered why creating a process on Unix based systems is using a
function called fork
, it's because under the hood, a
process is like a thread that executes the same assembly from the time
the application forked, but with a different memory space.
Why is this important to mention here? Our Unix API has a
PID
manager that keeps track of active process IDs, however
an issue might arise, when your application forks. In most cases, you
are probably going to make your application use fork in a way, where the
child is always busy, so that no further assembly is executed. However,
if this isn't the case our library WILL break your unmanaged child's
functionality.
Why is that? Because we want all processes to be created under the master process, we have specific code that guards all our Unix API from execution from child processes. In practice, we don't know how to manage your unmanaged slave, so instead we will directly kick it into an infinite loop. Keep this in mind if you think of having unmanaged children that can access the code.