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 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:

  1. 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.
  2. 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.
  3. The destroy function simply destroys the runner, but does not reinitialise it.
  4. The destroyForReuse function destroys the runner and reinitialises it.
  5. The valid function returns a bool that you can use to check it the runner is valid.
  6. The finished function returns a boolean, that you can use to check, whether the program ended its execution.
  7. On the contrary, the startable function returns whether you can start a new process from the runner.
  8. 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:

  1. Use it like the C++ one, by compiling the library and using the header file.
  2. To compile as a C library:
    1. Compile all files including the ones under the C folder.
    2. 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 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 };
    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 for 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. 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:

  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 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.