raw Software
RAW Software Programming Interprocess Communication

Pipe Data from Node.js to C

Robert Eisele

A named pipe, or FIFO, lets unrelated processes exchange a byte stream through a filesystem path. The stream has no built-in message boundaries or data types, so both sides must agree on a protocol. A newline-delimited text format is a practical choice for sending numbers from Node.js to C: it avoids assumptions about the size, byte order, and representation of a native double.

Create the FIFO Reader in C

The reader creates the FIFO with permissions limited to the current user. If the path already exists, it verifies that the existing object is actually a FIFO before opening it. Opening the read end blocks until a writer connects.

#define _POSIX_C_SOURCE 200809L

#include <ctype.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>

int main(int argc, char **argv) {
    const char *fifo_path = argc > 1 ? argv[1] : "/tmp/node-to-c.fifo";
    int created = 0;

    if (mkfifo(fifo_path, 0600) == 0) {
        created = 1;
    } else if (errno == EEXIST) {
        struct stat info;

        if (lstat(fifo_path, &info) == -1 || !S_ISFIFO(info.st_mode)) {
            fprintf(stderr, "%s exists but is not a FIFO\n", fifo_path);
            return EXIT_FAILURE;
        }
    } else {
        perror("mkfifo");
        return EXIT_FAILURE;
    }

    FILE *fifo = fopen(fifo_path, "r");
    if (fifo == NULL) {
        perror("fopen");
        if (created) unlink(fifo_path);
        return EXIT_FAILURE;
    }

    char line[256];
    while (fgets(line, sizeof line, fifo) != NULL) {
        char *end;

        if (strchr(line, '\n') == NULL) {
            int byte;
            while ((byte = fgetc(fifo)) != '\n' && byte != EOF) {}
            fprintf(stderr, "Ignoring oversized or unterminated record\n");
            continue;
        }

        errno = 0;
        double value = strtod(line, &end);
        while (isspace((unsigned char) *end)) end++;

        if (end == line || *end != '\0' || errno == ERANGE) {
            fprintf(stderr, "Ignoring invalid number: %s", line);
            continue;
        }

        printf("%.17g\n", value);
    }

    if (ferror(fifo)) {
        perror("fgets");
    }

    fclose(fifo);
    if (created) unlink(fifo_path);
    return EXIT_SUCCESS;
}

Compile the program with warnings enabled:

cc -std=c11 -Wall -Wextra -pedantic -Werror fifo-reader.c -o fifo-reader

Write Values from Node.js

The writer sends one decimal number per line. A writable stream buffers short bursts, while waiting for the drain event prevents an unbounded loop from queuing data faster than the operating system can accept it.

const fs = require("node:fs");
const { once } = require("node:events");

async function main() {
    const fifoPath = process.argv[2] ?? "/tmp/node-to-c.fifo";
    const values = [1.25, -2, Math.PI];
    const stream = fs.createWriteStream(fifoPath, { encoding: "utf8" });

    for (const value of values) {
        if (!stream.write(`${value}\n`)) {
            await once(stream, "drain");
        }
    }

    stream.end();
    await once(stream, "finish");
}

main().catch(error => {
    console.error(error);
    process.exitCode = 1;
});

Run Both Processes

Start the reader in one terminal. It waits at fopen until the write end is opened:

./fifo-reader /tmp/node-to-c.fifo

Then start the writer in another terminal:

node fifo-writer.cjs /tmp/node-to-c.fifo

The C process prints the three values and exits when Node closes the stream. A FIFO is local to one machine and does not persist queued data after both ends close. For a long-running service, keep the FIFO path under a private runtime directory, define how reconnects should work, and add record-size limits appropriate to the protocol.