C to Java Inter-Process Communication Examples
Tutorials and full code examples of how to use pipes and FIFOs to connect C/C++ compiled applications to a thread running inside a JVM using Java.
Update, 2026. This tutorial is from 2015, and the OS-level mechanics below (FIFOs,
popen, shared memory) still work exactly as written; pipes are forever. The Java side, though, has moved on a lot. Here’s the modern toolkit. The original 2015 walkthrough is preserved, unchanged, under Original version (2015) further down.
The modern way (2026 update)
Call C directly instead of piping bytes: the Foreign Function & Memory API
The biggest change since 2015 is that you often don’t need a pipe between C and Java at all. The
Foreign Function & Memory (FFM) API, finalized in JDK 22 (JEP 454,
the end of Project Panama), lets Java call C functions and share off-heap memory directly, with
no JNI, no glue C, no javah. You describe the C signature, get a MethodHandle, and call it:
import java.lang.foreign.*;
import java.lang.invoke.MethodHandle;
Linker linker = Linker.nativeLinker();
MethodHandle strlen = linker.downcallHandle(
linker.defaultLookup().find("strlen").orElseThrow(),
FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.ADDRESS));
try (Arena arena = Arena.ofConfined()) { // arena owns the native memory's lifetime
MemorySegment s = arena.allocateFrom("hello, world"); // Java String -> off-heap char*
long len = (long) strlen.invoke(s); // 12
}
For a real library you don’t even write that by hand. jextract reads a C header and
generates the bindings for you. And because a MemorySegment is just a safe pointer into
off-heap memory, two processes can map the same region and share it directly, which is the
clean version of the shared-memory note at the very end of the original post. Reach for FFM when
you want to call into native code or share a memory region, rather than stream bytes.
A cleaner channel than a FIFO: Unix domain sockets
If what you want is a bidirectional local channel, Java gained first-class Unix domain socket support in JDK 16 (JEP 380). It’s duplex, local-only, lower overhead than TCP loopback, and has none of the “a file that is secretly a pipe” surprises:
import java.net.*;
import java.nio.*;
import java.nio.channels.*;
import java.nio.file.Path;
var addr = UnixDomainSocketAddress.of(Path.of("/tmp/ipc.sock"));
try (var server = ServerSocketChannel.open(StandardProtocolFamily.UNIX)) {
server.bind(addr);
try (SocketChannel ch = server.accept()) { // the C side connects with AF_UNIX
ByteBuffer buf = ByteBuffer.allocate(1024);
ch.read(buf);
}
}
Endianness without htonl
The original converts byte order in C with htonl(). On the Java side you no longer have to
mirror that by hand. ByteBuffer.order(...) reads native-order bytes for you, so the C code can
skip the conversion entirely:
ByteBuffer buf = ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN);
channel.read(buf);
buf.flip();
int a = buf.getInt(); // already host-order, no htonl/ntohl dance
int b = buf.getInt();
(With FFM you do the same on a layout: ValueLayout.JAVA_INT.withOrder(ByteOrder.LITTLE_ENDIAN).)
Sturdier reads, and ProcessBuilder
Two of the original loops gate on in.ready() / available(). Those report what’s buffered
right now, not end-of-stream, so they can quietly stop early. Modern Java has blocking,
EOF-aware reads: InputStream.readNBytes(n) and readAllBytes() (JDK 9 / 11). And for spawning
the other process, ProcessBuilder with inheritIO(), redirectInput/Output(...), and
Process.onExit() replaces hand-rolling popen plumbing from the Java side.
What still holds up
Everything below. FIFOs and popen are still the simplest way to move a one-directional byte
stream between any two languages, and that’s exactly why they’re worth knowing. The decision
tree today: pipes to stream bytes, Unix domain sockets for a duplex channel, FFM to
call C or share memory. With that map in hand, here’s the original.
Original version (2015)
Everything below is the original August 2015 tutorial, unchanged.
How to connect a C/C++ process to a Java application
A question recently came up in the lab on how to connect a Java visualization to a C process, after a bit of thought I discovered that there aren’t too many sources that have documented methods so here are a few I came up with that work along with example code.
mkfifo
The first and probably the easiest method on Linux/Unix based machines is to use a FIFO. mkfifo is available on OS X, Linux and probably on Cygwin (I haven’t confirmed this). Using a pipe created with mkfifo from both the C and Java standpoint is as easy as opening and closing a regular file. To begin the process we start off by typing
mkfifo pipename
where pipename is the name we would like to give our FIFO.
mkfifo Example:
<img src=”../img/mkfifo.jpg” width = auto height = auto max-width: 50% alt=“ERROR” />
Next we have to set up the C and Java executables, the example program below reads in numbers from ap.txt then averages them in the compiled code and sends the result to the Java VM application to be printed.
C Application Side
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <inttypes.h>
#include <stdint.h>
/** define input buffer for below **/
#define BUFFER 4096
static const char* exFile = "ap.txt";
static char inputBuffer[ BUFFER ];
int main(int argc, const char** argv){
FILE *fp = fopen(exFile,"r");
/**
* check and see if the pointer is null in otherwords
* see if the memory location refered to by fp is set...no
* memory location should be zero if you want to reference
* it. Here are some good ways to do this other than the
* way I did it below:
* if(!fp) {do error}
* if(fp == NULL) {do error}
* and then there's the way I did it below:
*/
if(fp == 0 )
{
fprintf(stderr,"Null pointer exception, check file name.\n");
exit( EXIT_FAILURE );
}
//double check and see if an error occured during open
const int err = ferror(fp);
if(err != 0)
{
const char* errMessage = strcat("Something bad happened while opening file ",exFile);
perror( errMessage );
fclose( fp );
exit( EXIT_FAILURE );
}
setbuf(fp,inputBuffer); //set a buffer for input
uint64_t num = 0;
uint64_t total = 0;
uint64_t n = 0;
/**
* test for eof, feof(*fp) - returns a boolean 1 (true) if at end of
* file and 0 (false) otherwise
*/
while( !feof( fp ) )
{
/**
* fscanf returns the number of items it converted using the format
* that PRIu64 macro (defined in stdint.h) represents,
* if it's not equal to 1 we don't want to continue
*/
if( fscanf( fp,"%"PRIu64"", &num) != 1 )
{
/**
* you could do a lot of stuff here as far as error
* handling but basically something bad has happened
*/
break;
}
total += num; //add to total the value at memory location num
n++;
}
const double average = (double) total / (double) n;
//close the inputfile
fclose(fp);
//set outfile to FIFO name
static const char* outFile = "temp";
/**
* open FIFO for writing, we'll skip the open check
* since we just opened it externally...for production
* code you should check the return of fopen.
*/
FILE *fp_out = fopen(outFile,"w");
fprintf(fp_out,"%"PRIu64",%"PRIu64",%f\n",total,n,average);
/**
* close output FIFO, this leaves the FIFO but closes the
* file pointer
*/
fclose(fp_out);
return( EXIT_SUCCESS );
}
Java Application Side
import java.io.BufferedReader;
import java.io.IOException;
import java.io.FileReader;
public class StreamTest{
private static final String FIFO = "temp";
public static void main(String[] args){
BufferedReader in = null;
try{
System.out.println("JAVA SIDE!!");
in = new BufferedReader(new FileReader(FIFO));
while(in.ready()){
System.out.println(in.readLine());
}
//done, however you can choose to cycle over this line
//in this thread or launch another to check for new input
in.close();
}catch(IOException ex){
System.err.println("IO Exception at buffered read!!");
System.exit(-1);
}
}
}
The code for the above example can be downloaded here: javaio_fifo.tar.bz2
To run the code, change the javaio_fifo directory then type:
make StreamTest
make c_app
java -cp . StreamTest
./c_app
A more extensive explanation of the Linux mkfifo command can be found in your OS man pages by typing
man mkfifo.
When executing, you should see (illustrated below) “JAVA SIDE!!” followed by the total, number count and average from the c process.
###Pipe with Strings The second method opens a pipe directly from the C/C++ process using the popen( “target process”,“w”).
####Modifications to C Code above
static const char outFile = "java StreamTest";
FILE *fp_out = popen(outFile,"w";
fprintf(fp_out,"Total: %"PRIu64", Integers: %"PRIu64", Average: %.4f\n",total,n,average);
The Java process on the other hand changes to read from stdin input stream.
Modifications to Java Code above:
//remove this line
//import java.io.FileReader;
import java.io.InputStreamReader;
in = new BufferedReader(new InputStreamReader(System.in));
The full code base can be downloaded from:
javaio_pipes.tar.bz2.
As before change directory to the javaio_pipes directory, run the same make options
as before only this time no FIFO is created. To run the two applications we only need to
invoke the C process by typing:
./c_app
Once the process runs, the message JAVA SIDE: Total: xxx, Integers:
xxx, Average: xxx should appear. The complete process is illustrated
below.
Pipe with Data Stream Method
A third method is a slight modification of the second method. We still use popen but in this case we send data as raw bits instead of ascii encoded strings. Java reads numbers in big-endian format (see what is endianness) whereas C/C++ reads them in little-endian format (at least on x86 architectures) so we have a choice of either converting the numbers going into the pipe first to big-endian or converting them on the Java side once they are pulled out of the pipe. In my code example I’ve chosen to use the C method htonl() to convert the byte order in C.
C application side:
#include <stdlib.h>
#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>
#include <arpa/inet.h>
#include <math.h>
int main(int argc, char **argv )
{
srand(3);
int iterations = 1000;
FILE *out_put_log = fopen("c_output_log.csv","w");
FILE *fp = popen("java StreamTest","w");
if(fp == NULL){
fprintf(stderr,"null file pointer\n");
exit( EXIT_FAILURE );
}
const uint64_t size = sizeof(uint32_t);
while(iterations > 0){
uint32_t a = (uint32_t) rand();
uint32_t b = (uint32_t) rand();
fprintf(out_put_log,"%"PRIu32",%"PRIu32"\n",a,b);
a = htonl( a );
b = htonl( b );
fwrite(&a,size,1,fp);
fwrite(&b,size,1,fp);
iterations--;
}
fflush(fp);
fflush(out_put_log);
fclose(fp);
fclose(out_put_log);
return( EXIT_SUCCESS );
}
From the code we see that we generate two random unsigned 32-bit values, convert the endianness using htonl() and then write the values to the pipe using fwrite. I’ve used htonl, however you could just as easily use htobe32, however I’ve had issues using endian.h on OS X. This operation would be more efficient if I added the two 32-bit values into one 64-bit vector however for the sake of clarity I left the two writes.
Java application side:
import java.util.*;
import java.io.*;
import java.io.PrintWriter;
public class StreamTest{
public StreamTest(){
}
/**
* readStream - reads a stream from specified param stream, then adds it to array
* @param input_filename String
* @param length int
* @return List
*/
public ArrayList<Data> readStream(int length) throws EOFException,IOException{
DataInputStream input_stream = new DataInputStream(new BufferedInputStream(System.in,4096));
ArrayList<Data> array = new ArrayList<Data>();
Data<Integer> data = new Data<Integer>();
while(input_stream.available()>=4){
for(int i=0; i<length; i++){
data.arr.add(input_stream.readInt());
}
array.add(data);
data = new Data<Integer>();
}
return array;
}
public static void main(String[] args) throws FileNotFoundException{
ArrayList<Data> array = null;
StreamTest t = new StreamTest();
try{
array = t.readStream(2);
}catch(EOFException e){
System.err.println("We've reached the EOF signal!!");
}catch(IOException io){
System.err.println("We've hit an IO Exception!!");
}finally{
PrintWriter pw = new PrintWriter("java_output_log.csv");
for(Data<Integer> d:array){
pw.write(d.toString());
pw.write("\n");
}
if(!pw.checkError())
pw.close();
}
}
class Data<T>{
ArrayList<T> arr;
public Data(){
arr = new ArrayList<T>();
}
@Override
public String toString(){
StringBuilder sb = new StringBuilder();
for(int i = 0; i<arr.size(); i++){
sb.append(arr.get(i));
if(i != arr.size()-1)
sb.append(",");
}
return sb.toString();
}
}
}
The Java implementation reads in a list of two integers at a time and stores them in an ArrayList data structure. This method can be generalized all Java data types, however care must be taken when sending Java unsigned values since Java only has signed types.
The code for this example can be downloaded from here: javaio_pipes2.tar.bz2.
To run the example change the directory to javaio_pipes2 then type:
make c_app
make StreamTest
./c_app
Once c_app is executed it will open a pipe using popen() to the Java application then send through pairs of 32-bit values which are then read and stored in an output log “java_output_log.csv” and “c_output_log.csv” to convince doubters that this works you can run
diff java_output_log.c c_output_log.csv
and these two should match.
Notes
We’ve seen that OS level pipes and FIFOs are useful to communicate between the JVM and a compiled (C/C++) process. These principles can easily be applied to other language types (which is why I chose to highlight them). Another (perhaps more performance oriented) way to communicate is through POSIX shared memory, and it follows the same path as the last example. One complication with shared memory is the platform specific location in which you’ll open it (e.g., OS X, typically leaves the SHM file handles within the current working directory, whereas Linux will put the SHM handle in /dev/shm by default). If you want to try out the shared memory approach, check out the simplified SHM library from GitHub (https://github.com/jonathan-beard/shm), and integrate it with your project. You’ll have to open it from the Java side like a normal file and parse the input. If I get time I’ll update this tutorial with a Java FIFO example to make this more concrete. In the interim feel free to drop me a line and suggest improvements.