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.
The Spectrum
Section titled “The Spectrum”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 interopThe 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:
| Bridge | Host → Guest | Where you have seen it |
|---|---|---|
Python C API / ctypes / cffi | Python → C/C++/Rust | NumPy, PyTorch, Pillow — the fast core is native code |
| PyO3 / maturin | Python ↔ Rust | Polars, Pydantic v2, Ruff |
| Rcpp | R → C++ | Many fast CRAN packages |
| JNI | Java/Kotlin → C/C++ | Android NDK, native crypto libraries |
| P/Invoke | C# → C | Calling Win32 APIs and native DLLs |
dart:ffi | Dart → C | Flutter plugins for native features |
| WebAssembly | JS/anything → compiled modules | Polyglot 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:
| Mechanism | Notes |
|---|---|
| Unix domain socket | Fast local socket; common on Unix-like systems |
| Named pipe | Stream-like IPC; common on Windows and cross-platform runtimes |
| Shared memory | Very fast, but difficult correctness and ownership model |
| Memory-mapped file | Useful for large data handoff on one host |
| Local TCP | Slightly more overhead, but very convenient and portable |
| stdio protocol | Simple 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/2This is the normal shape of cross-language microservices.
Why This Tier Won
Section titled “Why This Tier Won”- 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 / HTTP + JSON
Section titled “REST / HTTP + JSON”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 / Protobuf
Section titled “gRPC / Protobuf”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
.protofile 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
.protofiles
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-webor 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 typesThe .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.
Other RPC Styles
Section titled “Other RPC Styles”| Style | Common use |
|---|---|
| GraphQL | client-selected fields, frontend aggregation, one endpoint |
| WebSocket | bidirectional session-oriented communication |
| JSON-RPC | simple method-call style over JSON |
| Thrift / Avro RPC | older or data-platform-heavy polyglot systems |
| DICOMweb / DIMSE | radiology imaging systems and PACS workflows |
The important point: at tier [5], languages stop directly calling each other. They exchange messages over a protocol.
Honorable Mentions
Section titled “Honorable Mentions”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# serviceHere, 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:
| Tool | Common cross-language role |
|---|---|
| Kafka | durable event streams; replayable consumers in many languages |
| RabbitMQ | work queues and flexible routing |
| AWS SQS / SNS | managed queues and pub/sub fan-out |
| NATS | lightweight polyglot messaging |
| Redis Streams | simple stream processing when Redis is already available |
| MQTT | telemetry and IoT messaging |
Common schema choices:
| Format | Notes |
|---|---|
| JSON | easiest to inspect, weakest typing |
| Protobuf | compact, typed, common with gRPC and event schemas |
| Avro | common in Kafka/data-platform ecosystems |
| CloudEvents | standard event envelope for event-driven systems |
| DICOM / HL7 / FHIR | healthcare-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:
| Format | Good for |
|---|---|
| CSV / TSV | simple tabular exchange |
| Excel | human-facing business workflows |
| JSONL | logs, events, semi-structured records |
| Parquet | analytical data lakes and columnar processing |
| DICOM | imaging studies and metadata |
| HL7 v2 / FHIR | healthcare interoperability |
| SQL tables | institutional 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.
What Is Possible vs. What Is Common
Section titled “What Is Possible vs. What Is Common”| Tier | Cross-language mechanism | Possible? | Common? | Typical use |
|---|---|---|---|---|
| [1–3] | FFI / native interop | yes | sometimes | performance-critical libraries, native APIs |
| [4] | same-host IPC | yes | sometimes | local workers, language servers, desktop/helper processes |
| [5] | REST / gRPC / network RPC | yes | very common | service-to-service and public APIs |
| [6] | async messaging | yes | very common | queues, events, background jobs |
| [7] | files / batch / data exchange | yes | very common | ETL, 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.
Practical Mental Model
Section titled “Practical Mental Model”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 recoverableA 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 serviceThat layering is normal. The trick is to keep each boundary explicit: native ABI, service API, event schema, or file/data contract.
The Architecture Lesson
Section titled “The Architecture Lesson”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.