Hacky GAP Communication

GAP
code
hacks
Published

February 11, 2024

While GAP provides a complete programming language, users often want to communicate with programs written in other languages. There are several ways to do that, including:

However, sometimes we need a lightweight connection. For this, I’ve found there is no easier option than sending strings (in my experience, JSON) down a pipe. This is precisely how the Vole package communicates with GAP. So, let’s discuss how to do that!

To do this, we are going to need three pieces:

What is a “pipe”? It acts much like its name – it acts like a (physical) pipe, where one program can write into it, and another can read out of it. Nowadays, the most common type of pipe is a network socket, but in our case (to start at least), we will be doing everything on one computer. Once you’ve created pipes, they all operate similarly, but each type is created differently. To start, we will use fifos.

You can call mkfifo from the command line to make a couple of pipes:

mkpipe togap.pipe
mkpipe fromgap.pipe

Most programming languages have a function that operates the same as the mkfifo program. GAP has IO_mkfifo, while C and C++ have mkfifo.

Let’s write two programs, one in GAP and one in C, to get some basic communication going! First, the GAP code:

# Write to this pipe
outfile := IO_File("fromgap.pipe", "w");
# Read from this pipe
infile := IO_File("togap.pipe", "r");
# Send a message. GAP promises to call "IO_Flush" as part of IO_WriteLine!
IO_WriteLine(outfile, "Hello from GAP!");
# Read a message
str := IO_ReadLine(infile);
Print("GAP read: ", str, "\n");

Then there is the C++ code:

#include <iostream>
#include <fstream>

int main(void)
{
    std::ifstream in("fromgap.pipe");
    std::ofstream out("togap.pipe");

    std::string line;
    std::getline(in, line);
    std::cout << "C++ read: " << line << "\n";
    // Using 'std::endl' makes sure our line is flushed
    out << "Hello from C++!" << std::endl;
    // We can also use .flush
    out.flush();
}

If you open two terminals and run these programs simultaneously, you should see GAP and C++ send messages to each other!

Problems you might have:

So, how can we send more exciting messages? One can look into complicated file formats, but my advice, certainly for starting, is JSON – one JSON object per line. Let’s extend our GAP example to a simple server, which can answer questions about the “transitive groups” library. Our GAP program will take a list whose first argument is the “command” to execute. Here is the GAP code:

  LoadPackage("json");

  # Write to this pipe
  outfile := IO_File("fromgap.pipe", "w");
  # Read from this pipe
  infile := IO_File("togap.pipe", "r");
  while true do
      # Grab the command, and convert it from JSON to GAP
      command := JsonStringToGap(IO_ReadLine(infile));
      if command[1] = "nrgroups" then
          ret := NrTransitiveGroups(command[2]);
      elif command[1] = "size" then
          ret := Size(TransitiveGroup(command[2], command[3]));
      elif command[1] = "exit" then
          QUIT_GAP();
      else
          Print("Broken Command :", command);
          QUIT_GAP();
      fi;
      # Convert the result back into JSON
      IO_WriteLine(outfile, GapToJsonString(ret));
  od;

Here is an example C++ client code, which can call our little server:

  #include <iostream>
  #include <fstream>
  #include <assert.h>

  int main(void)
  {
      std::ifstream in("fromgap.pipe");
      std::ofstream out("togap.pipe");

      // Get number of transitive groups on 6 points
      out << "[\"nrgroups\", 6]" << std::endl;
      std::string line;
      std::getline(in, line);
      // We will just use stoi here to convert the integer replies, rather than use a proper JSON parser.
      int count = std::stoi(line);
      // Remember that GAP's arrays start from 1!
      for(int i = 1; i <= count; ++i) {
          out << "[\"size\", 6, " << i << "]" << std::endl;
          std::getline(in, line);
          int size = std::stoi(line);
          std::cout << "TransitiveGroup(6," << i << ") has size " << size << "\n";
      }
      out << "[\"exit\"]" << std::endl;
  }

There are a number of advantages to using this kind of communication. In early development, if one of the programs crashes the other can continue – when given malformed input Vole will sometimes crash, but this isn’t fatal when it is run as a seperate program, as GAP can just notice the crash and continue (after giving a message to the user).