Skip to content

Cross-Language Communication: What's Possible vs. What's Common

The language boundary is one of the most important architectural seams in a software system. It decides whether two parts of the system share memory, share a process, share a protocol, or merely share data formats.

Here is how cross-language communication maps onto the communication tiers from the previous chapter.

TIGHT / FAST LOOSE / SLOW
ns ───────── μs ───────── ms ───────── s ───────── hours
│ │ │ │ │
[1-3] [4] [5] [6] [7]
▲ ▲ ▲
│ │ └── COMMONLY USED: REST, gRPC
│ └────────────── SOMETIMES: Unix sockets, shared memory
└─────────────────────────── TIGHTEST POSSIBLE: FFI / native interop

The short version:

  • Possible and fastest: call across languages inside one process using FFI.
  • Sometimes useful: communicate across processes on the same host using IPC.
  • Most common: expose a network API using REST/HTTP or gRPC.
  • Most resilient: communicate through messages, events, files, or batches.

The tighter options optimize latency. The looser options optimize isolation, deployment independence, and operational resilience.


Tightest Possible: [1–3] via FFI — Nanoseconds

Section titled “Tightest Possible: [1–3] via FFI — Nanoseconds”

FFI — Foreign Function Interface — lets one language call compiled functions from another language inside the same process. The two languages share an address space; once the bridge is set up, the call is essentially a direct function call.

┌────────────────────── One Process ──────────────────────┐
│ │
│ Python code │
│ │ │
│ ▼ FFI bridge │
│ Compiled C / Rust / C++ library │
│ (.so / .dll / .dylib) │
│ │
└─────────────────────────────────────────────────────────┘

The catch: one side almost always exposes a C-compatible ABI. You are not usually calling Python from Java directly. More often, both sides agree to speak C at the binary boundary.

Common bridges you have probably used without thinking about it:

BridgeHost → GuestWhere you have seen it
Python C API / ctypes / cffiPython → C/C++/RustNumPy, PyTorch, Pillow — the fast core is native code
PyO3 / maturinPython ↔ RustPolars, Pydantic v2, Ruff
RcppR → C++Many fast CRAN packages
JNIJava/Kotlin → C/C++Android NDK, native crypto libraries
P/InvokeC# → CCalling Win32 APIs and native DLLs
dart:ffiDart → CFlutter plugins for native features
WebAssemblyJS/anything → compiled modulesPolyglot execution in browsers, edge runtimes, and plugin sandboxes

This is the tightest level achievable across languages. You genuinely can get function-call latency.

But the trade-offs are serious:

  • You usually go through C, not directly between two high-level runtimes.
  • Memory management becomes tricky: who allocates, who frees, and who owns references?
  • A crash in native code can crash the whole process.
  • There is no process isolation.
  • Build and deploy complexity rises: cross-compilation, ABI compatibility, platform-specific binaries.
  • Debugging crosses language/runtime boundaries.

Use FFI for performance-critical inner loops, native platform integration, or reuse of mature compiled libraries. Do not use it as a general application-integration style.


Same-Host Cross-Language IPC: [4] — Microseconds

Section titled “Same-Host Cross-Language IPC: [4] — Microseconds”

A safer middle ground is to place each language runtime in its own process and communicate locally.

┌─────────────────┐ local IPC ┌─────────────────┐
│ Python process │ ───────────────────────▶ │ Rust process │
│ API / workflow │ ◀─────────────────────── │ fast worker │
└─────────────────┘ └─────────────────┘

Common IPC options:

MechanismNotes
Unix domain socketFast local socket; common on Unix-like systems
Named pipeStream-like IPC; common on Windows and cross-platform runtimes
Shared memoryVery fast, but difficult correctness and ownership model
Memory-mapped fileUseful for large data handoff on one host
Local TCPSlightly more overhead, but very convenient and portable
stdio protocolSimple parent/child process communication; common for language servers and CLI tools

This pattern is common when one language owns orchestration and another language owns a specialized runtime.

Examples:

  • a Python API service calling a Rust inference worker over a Unix socket
  • a Node/Electron app launching a Python helper process
  • a VS Code extension communicating with a language server over stdio or sockets
  • a web server delegating heavy ML work to a local process
  • a desktop app talking to a privileged local daemon

Compared with FFI, IPC gives you crash isolation and cleaner deployment boundaries. If the worker crashes, the host process can restart it. The cost is serialization, protocol design, and process lifecycle management.

Use same-host IPC when you want polyglot implementation with local performance, but you do not want native code loaded into the same process.


Commonly Used: [5] Network RPC — Milliseconds

Section titled “Commonly Used: [5] Network RPC — Milliseconds”

For most polyglot systems, the boundary is drawn at the process and network level. The dominant choices are REST/HTTP and gRPC.

┌────────────────┐ ┌────────────────┐
│ Service A │ ─── HTTP / JSON ───▶ │ Service B │
│ Python │ ◀──── response ───── │ Go │
└────────────────┘ └────────────────┘
or: gRPC / Protobuf over HTTP/2

This is the normal shape of cross-language microservices.

  • Every language has good HTTP libraries.
  • Most major languages have good gRPC support.
  • Processes are isolated from one another.
  • A crash in the Python service does not take down the Go service.
  • Services can be deployed and scaled independently.
  • Different teams can own different services.
  • The contract is an API, not a shared binary.

This is clean Dependency Inversion across languages: the services depend on a protocol contract, not each other’s implementation details.

REST over HTTP with JSON is the lingua franca of software integration.

Pros:

  • trivial to debug with curl, browser dev tools, Postman, or logs
  • works everywhere
  • human-readable payloads
  • excellent for public APIs and web frontends
  • weak coupling between client and server

Cons:

  • no schema enforcement by default
  • JSON is verbose and slower to parse than binary formats
  • inconsistent API design unless disciplined
  • weak streaming primitives compared with gRPC
  • versioning often becomes ad hoc

Use REST/HTTP when you need broad compatibility, public APIs, web/mobile integration, cross-organization workflows, and human-debuggable traffic.

gRPC plus Protocol Buffers is the typed, fast choice for cross-language service contracts. gRPC typically runs over HTTP/2, while Protobuf defines the schema and wire format.

Pros:

  • schema-first: a .proto file generates client and server stubs in every major language
  • efficient binary payloads — often around 5–10× smaller than JSON for comparable data
  • fast serialization/deserialization — often around 2–5× faster than JSON, depending on payload and runtime
  • good fit for internal service-to-service calls
  • HTTP/2 streaming: unary, server-streaming, client-streaming, and bidirectional streaming
  • clearer versioning discipline through .proto files

Cons:

  • harder to debug because the payload is binary
  • needs schema tooling and generated code
  • browser support is awkward compared with plain HTTP/JSON; it usually needs grpc-web or a gateway
  • less convenient for public, human-debuggable APIs
  • operationally more specialized than plain HTTP/JSON

Use gRPC when internal microservices need typed contracts, performance matters, and multiple languages need the same contract.

┌────────────── shared.proto ──────────────┐
│ message Study { │
│ string id = 1; │
│ string accession_number = 2; │
│ string modality = 3; │
│ } │
└──────────────────┬───────────────────────┘
│ codegen
┌──────────┼──────────┬───────────┐
▼ ▼ ▼ ▼
┌────────────┐ ┌────────┐ ┌───────────┐ ┌────────────┐
│ Python stub│ │ Go stub│ │ Dart stub │ │ C# stub │
└────────────┘ └────────┘ └───────────┘ └────────────┘
same wire format, same field numbers, same types

The .proto file becomes the single source of truth across languages. That is genuinely lovely: Dependency Inversion at the system level. Services depend on the shared contract, not on each other’s implementation details.

StyleCommon use
GraphQLclient-selected fields, frontend aggregation, one endpoint
WebSocketbidirectional session-oriented communication
JSON-RPCsimple method-call style over JSON
Thrift / Avro RPColder or data-platform-heavy polyglot systems
DICOMweb / DIMSEradiology imaging systems and PACS workflows

The important point: at tier [5], languages stop directly calling each other. They exchange messages over a protocol.

Tier [4] local IPC across languages is absolutely possible: Unix sockets, named pipes, shared memory, memory-mapped files, stdio protocols, and local TCP. It is just less common as the primary application pattern. If components are already on the same host, teams often choose either:

  • full FFI for maximum speed, or
  • full network RPC for clarity and operational familiarity.

The middle ground still matters in a few important places:

  • editor ↔ language server, such as LSP over stdio or JSON-RPC
  • Jupyter kernel ↔ frontend, often through ZeroMQ
  • browser ↔ native helper, such as native messaging
  • desktop app ↔ local daemon or privileged helper
  • orchestration process ↔ local inference worker

Tier [6] async messaging is heavily polyglot in practice. Brokers do not care whether producers and consumers are written in Python, Go, Rust, Java, C#, or Dart, as long as everyone agrees on the message schema — often Protobuf, Avro, JSON Schema, or CloudEvents.

This is probably the second most common cross-language pattern after REST/gRPC, especially in event-driven architectures and data pipelines.


Asynchronous Cross-Language Communication: [6] — Milliseconds to Seconds

Section titled “Asynchronous Cross-Language Communication: [6] — Milliseconds to Seconds”

When the caller does not need an immediate response, asynchronous messaging is often better than RPC.

┌──────────────┐ event/message ┌──────────────┐
│ Python API │ ───────────────────────▶ │ Kafka / NATS │
└──────────────┘ │ RabbitMQ/SQS │
└──────┬───────┘
┌────────────────────────┼──────────────────────┐
▼ ▼ ▼
Go consumer Rust worker C# service

Here, the producer and consumer do not need to share a runtime, a process, or even a deployment schedule. They only need to agree on message semantics.

Common tools:

ToolCommon cross-language role
Kafkadurable event streams; replayable consumers in many languages
RabbitMQwork queues and flexible routing
AWS SQS / SNSmanaged queues and pub/sub fan-out
NATSlightweight polyglot messaging
Redis Streamssimple stream processing when Redis is already available
MQTTtelemetry and IoT messaging

Common schema choices:

FormatNotes
JSONeasiest to inspect, weakest typing
Protobufcompact, typed, common with gRPC and event schemas
Avrocommon in Kafka/data-platform ecosystems
CloudEventsstandard event envelope for event-driven systems
DICOM / HL7 / FHIRhealthcare-specific data and workflow standards

Use async messaging when you want:

  • work distribution across languages
  • event fan-out to multiple consumers
  • independent scaling
  • buffering during traffic spikes
  • resilience to temporary downstream failure
  • event-driven workflows

The cost is eventual consistency and operational complexity. You must handle retries, duplicate delivery, idempotency, ordering, schema evolution, and tracing across asynchronous boundaries.


Loosest Cross-Language Boundary: [7] Batch / File / Data Exchange — Seconds to Hours

Section titled “Loosest Cross-Language Boundary: [7] Batch / File / Data Exchange — Seconds to Hours”

The loosest integration style is data exchange. One system writes data. Another system reads it later.

┌──────────────┐ writes/export ┌────────────────────┐
│ R pipeline │ ────────────────────────▶ │ S3 / FTP / DB │
└──────────────┘ │ shared folder │
└────────┬───────────┘
│ later
┌────────────────────┐
│ Python / SQL / BI │
│ importer │
└────────────────────┘

Examples:

  • R exports a CSV; Python reads it later.
  • A hospital system drops DICOM studies into a research archive.
  • An ETL job writes Parquet files to a data lake.
  • A partner uploads files to SFTP.
  • A SaaS product sends webhook callbacks when processing is complete.
  • A legacy system writes rows into a shared integration database.

Formats matter more than languages at this tier:

FormatGood for
CSV / TSVsimple tabular exchange
Excelhuman-facing business workflows
JSONLlogs, events, semi-structured records
Parquetanalytical data lakes and columnar processing
DICOMimaging studies and metadata
HL7 v2 / FHIRhealthcare interoperability
SQL tablesinstitutional reporting and operational integration

This pattern is slow, but highly practical. It is especially common across organizations because it is auditable, inspectable, and easy to recover manually when something goes wrong.

Use batch/file exchange when the boundary is organizational, legacy, regulated, offline, or not latency-sensitive.


TierCross-language mechanismPossible?Common?Typical use
[1–3]FFI / native interopyessometimesperformance-critical libraries, native APIs
[4]same-host IPCyessometimeslocal workers, language servers, desktop/helper processes
[5]REST / gRPC / network RPCyesvery commonservice-to-service and public APIs
[6]async messagingyesvery commonqueues, events, background jobs
[7]files / batch / data exchangeyesvery commonETL, cross-org exchange, healthcare/enterprise integration

The surprising lesson is that the fastest option is not the default option. Most production systems choose process or network boundaries because they are easier to deploy, observe, recover, and evolve.


Need maximum speed, willing to share a process?
→ FFI: Python ↔ Rust/C++ [1–3]
rare, surgical, performance-critical
Need typed contracts across internal services?
→ gRPC + Protobuf [5]
common, modern, schema-first
Need to expose APIs to browsers, partners, or humans?
→ REST + JSON [5]
ubiquitous, debuggable, integration-friendly
Need decoupled, scalable, event-driven workflows?
→ Kafka / RabbitMQ / NATS + Protobuf or Avro [6]
common in data pipelines and event-driven systems
Need cross-organization, legacy, or audit-friendly exchange?
→ files / batch / database handoff [7]
slow but honest and operationally recoverable

A practical heuristic:

Cross language only at the tightest level your problem truly requires.

If the problem is performance-critical numerical code, FFI is worth it. If the problem is ordinary product or service integration, REST/gRPC is usually the right default. If the problem is resilience, scaling, and workflow decoupling, use messaging. If the problem crosses institutions, legacy systems, or audit-heavy environments, batch/file exchange may be the most honest architecture.

In a Radiology AI Unit context, you might see all of these layered together:

Python inference service
└─ wraps C++/CUDA model via FFI [1–3]
└─ exposes REST or gRPC API to .NET orchestrator [5]
└─ publishes "inference complete" event [6]
└─ consumed by Dart/Flutter notification service

That layering is normal. The trick is to keep each boundary explicit: native ABI, service API, event schema, or file/data contract.


Cross-language communication is less about syntax and more about where the boundary lives.

  • FFI says: “Different languages, same process.”
  • IPC says: “Different languages, same machine.”
  • RPC says: “Different languages, different services.”
  • Messaging says: “Different languages, independent workflows.”
  • Batch exchange says: “Different languages, different organizations or time scales.”

The more tightly you bind languages together, the more you optimize runtime speed. The more loosely you connect them, the more you optimize human and operational independence.

That is usually the real design decision.