Skip to content

Instantly share code, notes, and snippets.

@matkatmusic
Created August 3, 2025 05:46
Show Gist options
  • Save matkatmusic/cd0bad0ff6f84152339a8788a2f3a311 to your computer and use it in GitHub Desktop.
Save matkatmusic/cd0bad0ff6f84152339a8788a2f3a311 to your computer and use it in GitHub Desktop.
a complete example project for using the juce::InterprocessConnection and juce::InterprocessConnectionServer classes
/*
==============================================================================
This file contains the basic startup code for a JUCE application.
==============================================================================
*/
#include <JuceHeader.h>
/*
Background:
I was looking for an example of how to use the juce InterprocessConnection class and InterprocessConnectionServer class.
The forum was bleak but there were some snippets that could be pieced together to create a working example.
The example below is a simple client/server application that uses the InterprocessConnection classes to send messages back and forth.
The code is structured so that the important parts that make it all work are near the top, and the unimportant parts are below the main class.
The exception to this format is the implementation of the `messageReceived` functions in the ServerSideConnection and ClientSideConnection classes.
These implementations are long, as they demonstrate *a* way to implement something like the 'sequence of events' shown a few lines down.
In terminal window #1: launch with "-mode server -port 1234"
Once launched, in Terminal window #2: launch with "-mode client -host localhost -port 1234"
sequence of events:
Terminal Window #1: server starts listening on port <port>
Terminal Window #2: client opens socket connecting to <server url>:<port>
if connection succeeds, the code below is set up so that the server begins the dialog:
server -> client: "Hello". { "Type": "Message", "Data": {"Value": "Hello"} }
client -> server: "I got your "hello". {type: message, data: {value: HelloReceived} }
client -> server: "hello"
server -> client: "I got your hello"
server -> client: "Here's a value: " { type: message, data: {Value: Value} }
server -> client: "42" { type: value, value: 42 }
client -> server: "I got your value:" {type: message, data { Value: ValueReceived, Data: 42 }
client -> server: "here's a value:" { type: message, data: {Value: Value} }
client -> server: "43" { type: value, value: 43 }
server -> client: "I got your value: " {type: message, data { Value: ValueReceived, Data: 43 }
client -> server: "good bye" {type: message, data { Value: Goodbye }
server -> client: "I got your goodbyte" {type: message, data { Value: GoodbyeReceived }
client disconnects and shuts down (Terminal Window #2 program exits)
server shuts down (Terminal Window #1 program exits)
*/
/*
Some messaging-type blurbs to eliminate the use of plain-text strings in the messages being sent between the client and server
*/
#define DECLARE_ID(name) const juce::Identifier name (#name);
namespace IDs
{
DECLARE_ID(Type)
DECLARE_ID(Data)
DECLARE_ID(Value)
}
enum class Types
{
Message, Value,
};
enum class Values
{
Hello, HelloReceived,
Value, ValueReceived,
Goodbye, GoodbyeReceived,
};
/*
A wrapper class around the juce::var type that allows it to be treated like a JSON object.
the main purpose is to make setting properties easier and hide interacting with the underlying juce::DynamicObject.
*/
struct JsonObject
{
JsonObject() { json = juce::var( new juce::DynamicObject()); }
operator juce::var() { return json; }
void setProperty(const juce::Identifier& id, juce::var value);
void setProperty(const juce::Identifier& id, JsonObject obj);
template<typename T>
void setProperty(const juce::Identifier& id, T value);
private:
juce::var json;
void setPropInternal(const juce::Identifier& id, juce::var value);
};
/*
A helper functions that hides the C++-ness of converting a juce::var property to a specific type, if you know that the property was stored as an int.
*/
template<typename T>
T fromInt(juce::var json, juce::Identifier id);
/*
a function that hides the details of sending a message to the other side of the connection.
*/
void sendMessage(juce::InterprocessConnection& connection, juce::var json);
/*
This is the InterprocessConnection class instance that will live on the Server side.
It processes messages sent by the remote client.
It is automatically created and owned by the ServerSideConnectionFactory class whenever a new client connects to the server.
the connectionMade() function is where I decided the sequence of messages should begin.
Every other message exchanged between the client and server is handled in the messageReceived() function.
If the client disconnects, the application will be closed.
*/
class ServerSideConnection : public juce::InterprocessConnection
{
public:
ServerSideConnection() {}
~ServerSideConnection() override;
void connectionMade() override
{
DBG( "ServerSideConnection connection made.");
/*
{
"Type": "Message",
"Data": {
"Value": "Hello"
}
}
*/
auto json = JsonObject();
json.setProperty(IDs::Type, Types::Message);
auto data = JsonObject();
data.setProperty(IDs::Value, Values::Hello);
json.setProperty(IDs::Data, data);
::sendMessage(*this, json);
}
void connectionLost() override;
void messageReceived(const juce::MemoryBlock& message) override;
};
/*
This is the class that listens for incoming connections.
Every time a client connects to the port specified, a new ServerSideConnection instance is created.
that ServerSideConnection instance is what does the actual work of communicating with the client.
*/
class ServerSideConnectionFactory : public juce::InterprocessConnectionServer
{
public:
ServerSideConnectionFactory(int tcpPortToMakeAvailable)
{
if( beginWaitingForSocket(tcpPortToMakeAvailable) )
{
DBG( "ServerSideConnectionFactory is active, listening to port " << tcpPortToMakeAvailable );
}
}
juce::InterprocessConnection * createConnectionObject () override
{
clients.push_back( std::make_unique<ServerSideConnection>() );
return clients.back().get();
}
~ServerSideConnectionFactory() override { stop(); }
private:
std::vector<std::unique_ptr<ServerSideConnection>> clients;
};
/*
This class represents the client side of the connection.
it's first job is to try to connect to the server residing at <serverIP>:<portServerIsListeningOn>.
After that connection is made, this design will simply wait for the server to send something before sending messages of its own.
If the connection is lost/closed, it will print a message to the debug console and the program will exit.
*/
class ClientSideConnection : public juce::InterprocessConnection
{
public:
ClientSideConnection(juce::String serverIP, int portServerIsListeningOn)
{
if( connectToSocket(serverIP, portServerIsListeningOn, 3000) )
{
DBG( "connected to host: " << serverIP << ":" << portServerIsListeningOn << " successfully" );
}
}
~ClientSideConnection() override;
void connectionMade() override;
void connectionLost() override;
void messageReceived(const juce::MemoryBlock& message) override;
};
//==============================================================================
class TCPUtilityApplication : public juce::JUCEApplication
{
public:
/*
The initialise() function is where the commandline args are parsed.
It's also where the client or server is created, depending on the commandline args.
*/
//==============================================================================
void initialise (const juce::String& commandLine) override
{
const auto args = juce::ArgumentList(getApplicationName(), commandLine);
if( args.containsOption("-mode") && args.containsOption("-port"))
{
auto portToUse = args.getValueForOption("-port").getIntValue();
auto mode = args.getValueForOption("-mode");
if( mode == "client" )
{
if( args.containsOption("-host") == false )
{
DBG( "specify the host to connect to via -host <host>. ");
systemRequestedQuit();
return;
}
auto serverIP = args.getValueForOption("-host");
client = std::make_unique<ClientSideConnection>(serverIP, portToUse);
}
else if( mode == "server" )
{
if( args.containsOption("-port") == false )
{
DBG( "specify the host port to connect to via -port <port>" );
systemRequestedQuit();
return;
}
server = std::make_unique<ServerSideConnectionFactory>(portToUse);
}
}
else
{
DBG( "usage: -mode <client or server> -host <host> -port <port>. host is only required in client mode");
systemRequestedQuit();
return;
}
}
/*
Depending on which mode you're running the app in, the shutdown() function will clean up the client or server.
*/
void shutdown() override
{
if( client )
client.reset();
if( server )
server.reset();
}
//==============================================================================
/*
The rest of these functions are just boilerplate code that JUCE applications need.
*/
TCPUtilityApplication() {}
void systemRequestedQuit() override { quit(); }
void anotherInstanceStarted (const juce::String& commandLine) override { }
const juce::String getApplicationName() override { return ProjectInfo::projectName; }
const juce::String getApplicationVersion() override { return ProjectInfo::versionString; }
bool moreThanOneInstanceAllowed() override { return true; }
//==============================================================================
private:
std::unique_ptr<ClientSideConnection> client;
std::unique_ptr<ServerSideConnectionFactory> server;
};
/*
Forward declarations needed for the messageReceived implementations of both ServerSide and ClientSide classes
*/
namespace Stream
{
namespace Writers
{
bool writeString (const juce::String& value, juce::OutputStream& output);
}//end namespace Writers
namespace Readers
{
bool checkBytesAvailable (juce::int64 requiredBytes, const char* message, juce::InputStream& input);
void readPaddingZeros (size_t bytesRead, juce::InputStream& input);
juce::String readString(juce::MemoryInputStream& input);
} //end namespace Readers
} //end namespace Stream
juce::MemoryBlock convertJsonToMemoryBlock(const juce::var& json);
juce::var convertMemoryBlockToJson(const juce::MemoryBlock& block);
//================================================================================
void ServerSideConnection::messageReceived(const juce::MemoryBlock& message)
{
DBG( "ServerSideConnection message received");
auto json = convertMemoryBlockToJson(message);
DBG( juce::JSON::toString(json));
jassert(json.hasProperty(IDs::Type));
auto type = fromInt<Types>(json, IDs::Type);
switch (type)
{
case Types::Message:
{
jassert(json.hasProperty(IDs::Data));
auto data = json[IDs::Data];
if( data.hasProperty(IDs::Value) )
{
auto value = fromInt<Values>(data, IDs::Value);
switch (value)
{
case Values::Hello: //client sent hello
{
DBG( "the client sent hello");
//jassertfalse;
//server -> client: "I got your hello"
auto response = JsonObject();
response.setProperty(IDs::Type, Types::Message);
auto responseData = JsonObject();
responseData.setProperty(IDs::Value, Values::HelloReceived);
response.setProperty(IDs::Data, responseData);
::sendMessage(*this, response);
//server -> client: "Here's a value: " { type: message, data: {Value: Value} }
response = JsonObject();
response.setProperty(IDs::Type, Types::Message);
responseData = JsonObject();
responseData.setProperty(IDs::Value, Values::Value);
response.setProperty(IDs::Data, responseData);
::sendMessage(*this, response);
//server -> client: "42" { type: value, value: 42 }
response = JsonObject();
response.setProperty(IDs::Type, Types::Value);
response.setProperty(IDs::Value, 42);
::sendMessage(*this, response);
break;
}
case Values::HelloReceived:
{
DBG( "client received our Hello");
//jassertfalse;
break;
}
case Values::Value:
{
DBG( "the client is sending a value");
//jassertfalse;
break;
}
case Values::ValueReceived:
{
DBG( "client received our value");
//jassertfalse;
break;
}
case Values::Goodbye:
{
DBG( "the client is saying goodbye");
//jassertfalse;
auto response = JsonObject();
response.setProperty(IDs::Type, Types::Message);
auto responseData = JsonObject();
responseData.setProperty(IDs::Value, Values::GoodbyeReceived);
response.setProperty(IDs::Data, responseData);
::sendMessage(*this, response);
break;
}
case Values::GoodbyeReceived:
{
DBG("client received our goodbye");
jassertfalse;
break;
}
}
}
break;
}
case Types::Value:
{
DBG( "the client sent a value");
//jassertfalse;
//server -> client: "I got your value: " {type: message, data: { Value: ValueReceived, Data: 43 }
jassert(json.hasProperty(IDs::Value));
int valueFromClient = json[IDs::Value];
auto response = JsonObject();
response.setProperty(IDs::Type, Types::Message);
auto responseData = JsonObject();
responseData.setProperty(IDs::Value, Values::ValueReceived);
responseData.setProperty(IDs::Data, valueFromClient);
response.setProperty(IDs::Data, responseData);
::sendMessage(*this, response);
}
}
}
//================================================================================
void ClientSideConnection::messageReceived(const juce::MemoryBlock& message)
{
DBG( "ClientSideConnection message received" );
auto json = convertMemoryBlockToJson(message);
DBG( juce::JSON::toString(json));
jassert(json.hasProperty(IDs::Type));
auto type = fromInt<Types>(json, IDs::Type);
switch (type)
{
case Types::Message:
{
jassert(json.hasProperty(IDs::Data));
auto data = json[IDs::Data];
if( data.hasProperty(IDs::Value) )
{
auto value = fromInt<Values>(data, IDs::Value);
switch (value)
{
case Values::Hello: //server sent "Hello"
{
DBG( "the server said hello" );
//jassertfalse;
//client -> server: "I got your "hello". {type: message, data: {value: HelloReceived} }
auto response = JsonObject();
response.setProperty(IDs::Type, Types::Message);
auto responseData = JsonObject();
responseData.setProperty(IDs::Value, Values::HelloReceived);
response.setProperty(IDs::Data, responseData);
::sendMessage(*this, response);
//client -> server: "hello"
response = JsonObject();
response.setProperty(IDs::Type, Types::Message);
responseData = JsonObject();
responseData.setProperty(IDs::Value, Values::Hello);
response.setProperty(IDs::Data, responseData);
::sendMessage(*this, response);
break;
}
case Values::HelloReceived:
{
DBG( "the server received our hello");
//jassertfalse;
break;
}
case Values::Value:
{
DBG( "the server is sending a value");
//jassertfalse;
break;
}
case Values::ValueReceived:
{
DBG( "the server received our Value");
// client -> server: "good bye" {type: message, data { Value: Goodbye }
auto response = JsonObject();
response.setProperty(IDs::Type, Types::Message);
auto responseData = JsonObject();
responseData.setProperty(IDs::Value, Values::Goodbye);
response.setProperty(IDs::Data, responseData);
::sendMessage(*this, response);
break;
}
case Values::Goodbye:
{
DBG("the server is saying goodbye");
jassertfalse;
break;
}
case Values::GoodbyeReceived:
{
DBG( "the server received our Goodbye");
disconnect();
juce::JUCEApplication::getInstance()->systemRequestedQuit();
break;
}
}
}
break;
}
case Types::Value:
{
DBG ("the server sent a value");
//jassertfalse;
jassert(json.hasProperty(IDs::Value));
int valueFromServer = json[IDs::Value];
//client -> server: "I got your value:" {type: message, data { Value: ValueReceived, Data: 42 }
auto response = JsonObject();
response.setProperty(IDs::Type, Types::Message);
auto responseData = JsonObject();
responseData.setProperty(IDs::Value, Values::ValueReceived);
responseData.setProperty(IDs::Data, valueFromServer);
response.setProperty(IDs::Data, responseData);
::sendMessage(*this, response);
// client -> server: "here's a value:" { type: message, data: {Value: Value} }
response = JsonObject();
response.setProperty(IDs::Type, Types::Message);
responseData = JsonObject();
responseData.setProperty(IDs::Value, Values::Value);
response.setProperty(IDs::Data, responseData);
::sendMessage(*this, response);
// client -> server: "43" { type: value, value: 43 }
response = JsonObject();
response.setProperty(IDs::Type, Types::Value);
response.setProperty(IDs::Value, 43);
::sendMessage(*this, response);
break;
}
}
}
//================================================================================
/*
Finally, here are all the implementations that we don't care about as much.
*/
ServerSideConnection::~ServerSideConnection()
{
disconnect();
}
void ServerSideConnection::connectionLost()
{
DBG( "ServerSideConnection connection lost. Shutting down" );
juce::JUCEApplication::getInstance()->systemRequestedQuit();
}
ClientSideConnection::~ClientSideConnection() { disconnect(); }
void ClientSideConnection::connectionMade() { DBG( "ClientSideConnection connection made to " << getConnectedHostName() ); }
void ClientSideConnection::connectionLost()
{
DBG( "ClientSideConnection connection lost" );
juce::JUCEApplication::getInstance()->systemRequestedQuit();
}
void JsonObject::setProperty(const juce::Identifier& id, juce::var value)
{
setPropInternal(id, value);
}
void JsonObject::setProperty(const juce::Identifier& id, JsonObject obj)
{
setPropInternal(id, static_cast<juce::var>(obj));
}
template<typename T>
void JsonObject::setProperty(const juce::Identifier& id, T value)
{
setPropInternal(id, static_cast<int>(value));
}
void JsonObject::setPropInternal(const juce::Identifier& id, juce::var value)
{
json.getDynamicObject()->setProperty(id, value);
}
void sendMessage(juce::InterprocessConnection& connection, juce::var json)
{
DBG( "sending: ");
DBG( juce::JSON::toString(json));
DBG( "" );
connection.sendMessage(convertJsonToMemoryBlock(json));
}
template<typename T>
T fromInt(juce::var json, juce::Identifier id)
{
jassert( json.hasProperty(id));
return static_cast<T>( json[id].toString().getIntValue() );
}
bool Stream::Writers::writeString (const juce::String& value, juce::OutputStream& output)
{
if (! output.writeString (value))
return false;
const size_t numPaddingZeros = ~value.getNumBytesAsUTF8() & 3;
return output.writeRepeatedByte ('\0', numPaddingZeros);
}
juce::String Stream::Readers::readString(juce::MemoryInputStream& input)
{
checkBytesAvailable (4, "Stream::Reader input stream exhausted while reading string", input);
auto posBegin = (size_t) input.getPosition();
auto s = input.readString();
auto posEnd = (size_t) input.getPosition();
if (static_cast<const char*> (input.getData()) [posEnd - 1] != '\0')
{
juce::Logger::writeToLog ("Stream::Reader input stream exhausted before finding null terminator of string");
jassertfalse;
return {};
}
size_t bytesRead = posEnd - posBegin;
readPaddingZeros (bytesRead, input);
return s;
}
void Stream::Readers::readPaddingZeros (size_t bytesRead, juce::InputStream& input)
{
size_t numZeros = ~(bytesRead - 1) & 0x03;
while (numZeros > 0)
{
if (input.isExhausted() || input.readByte() != 0)
{
juce::Logger::writeToLog ("Stream::Reader input stream format error: missing padding zeros");
jassertfalse;
break;
}
--numZeros;
}
}
bool Stream::Readers::checkBytesAvailable (juce::int64 requiredBytes, const char* message, juce::InputStream& input)
{
if (input.getNumBytesRemaining() < requiredBytes)
{
juce::Logger::writeToLog (message);
jassertfalse;
return false;
}
return true;
}
juce::MemoryBlock convertJsonToMemoryBlock(const juce::var& json)
{
juce::MemoryBlock mb;
{
auto output = juce::MemoryOutputStream(mb, false);
Stream::Writers::writeString( juce::JSON::toString(json), output);
}
return mb;
}
juce::var convertMemoryBlockToJson(const juce::MemoryBlock& block)
{
auto input = juce::MemoryInputStream(block, false);
auto str = Stream::Readers::readString(input);
return juce::JSON::fromString(str);
}
//==============================================================================
// This macro generates the main() routine that launches the app.
START_JUCE_APPLICATION (TCPUtilityApplication)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment