C++ API

The C++ API has 2 ways of executing your program:

  1. Using the execandwait function
  2. 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 file 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 init function takes the same arguments as execandwait but it has additional arguments. These additional arguments are default booleans that toggle different pipes on(STDIN, STDOUT, STDERR). The init function returns the same result as execandwait.

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 really check if

The destroy function simply destroys the runner, but does not reinitialize it, to do that you need to call destroyForReuse

The valid function returns a bool that you can use to check it the runner is valid

The finished function returns a boot that you can check it 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. To function you must have initialized

C API

There are 2 ways to use the C API, the first one is to 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.

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 simple functions that take a data struct that contains the data used by the class. 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 of the C++ API

void execfunc()
{
    char* const args[] = { (char*)"https://madladsquad.com/testingdt", nullptr };
    uexec::ScriptRunner runner{};
    runner.init(args, false, true, false);
    runner.update();

    std::string buff;
    buff.resize(256);
    size_t bytes = 0;
    std::this_thread::sleep_for(std::chrono::seconds(2));
    runner.readSTDOUT(buff, buff.size(), bytes);
    buff.erase(bytes);
    std::cout << bytes << std::endl;
    std::cout << buff << std::endl;
}

Here is an example of the C API

void execfunc()
{
    char* const args[] = { (char*)"https://madladsquad.com/testingdt", nullptr };
    RunnerData data{};
    uexec_runner_init(&data, args, false, true, false);
    uexec_runner_update(&data);

    std::string buff;
    buff.resize(256);
    size_t bytes = 0;

    std::this_thread::sleep_for(std::chrono::seconds(2));
    uexec_runner_readSTDOUT(&data, buff.data(), buff.size(), &bytes);
    buff.erase(bytes);
    std::cout << bytes << std::endl;
    std::cout << buff << std::endl;
}

while this is written in C++, it's still using the C API and there will be no difference if you replaced the calls to standard libraries

Handling process termination and reusing runners

In this example 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

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:

  1. You explicitly terminated a process by calling the terminate() function
  2. 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, and you want to reuse the process call destroyForReuse() which will reinitialize the class and will mark it as a valid one. After that you can call init again

The destroyForReuse function as said before re-initializes the process so that it can be reused. Under the hood it checks if the process has finished execution, if it hasn't calls destroy and then re-initializes 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 has a different memory space.

Why is this important to mention here? Well our Unix API has a PID manager that keeps track of active process IDs, however the issue comes 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