Hacky GAP Communication
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:
- If you want to work with code written in Python, you can use Sagemath.
- If you want to work with code written in Julia, use GAP.jl, which is part of the Oscar System.
- C and C++ code can be integrated into GAP using a GAP kernel package (but this is not for the faint of heart!).
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:
- A GAP program.
- Another program in the language of your choice (I’m going to use C++).
- Some “pipes” for the programs to talk through.
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
:= IO_File("fromgap.pipe", "w");
outfile # Read from this pipe
:= IO_File("togap.pipe", "r");
infile # Send a message. GAP promises to call "IO_Flush" as part of IO_WriteLine!
"Hello from GAP!");
IO_WriteLine(outfile, # Read a message
str := IO_ReadLine(infile);
"GAP read: ", str, "\n"); Print(
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
<< "Hello from C++!" << std::endl;
out // We can also use .flush
.flush();
out}
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:
- The programs ran, but there were no messages – did you run
mkfifo fromgap.pipe
andmkfifo togap.pipe
in advance to make the magic pipe files? - I ran one of the programs and it just seems “stuck” – one of the useful (but confusing) features of pipes is if one program opens a pipe for reading, the program will wait until another program opens the same pipe for writing (and vice-versa). This means it is important that the order in which the pipes are opened must be the same, or both programs will be stuck forever!
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:
"json");
LoadPackage(
# Write to this pipe
:= IO_File("fromgap.pipe", "w");
outfile # Read from this pipe
:= IO_File("togap.pipe", "r");
infile while true do
# Grab the command, and convert it from JSON to GAP
:= JsonStringToGap(IO_ReadLine(infile));
command if command[1] = "nrgroups" then
:= NrTransitiveGroups(command[2]);
ret elif command[1] = "size" then
:= Size(TransitiveGroup(command[2], command[3]));
ret elif command[1] = "exit" then
;
QUIT_GAP()else
"Broken Command :", command);
Print(;
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
<< "[\"nrgroups\", 6]" << std::endl;
out 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) {
<< "[\"size\", 6, " << i << "]" << std::endl;
out std::getline(in, line);
int size = std::stoi(line);
std::cout << "TransitiveGroup(6," << i << ") has size " << size << "\n";
}
<< "[\"exit\"]" << std::endl;
out }
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).