Introduction
I've experienced many times a problem where we had an implementation in one programming language in one component and then needed the same thing on a different layer written in a different language. This problem can be solved in many ways, and different people prefer different solutions. My preferred approach would be to have a native implementation in each language, but this requires reimplementation. The ideal solution would be to have a single implementation that serves as a source for transpilation. But let's start from the beginning.
Portability and Reusability
Portability and reusability are two holy grails of software engineering. Portability allows a piece of software to be ported across different environments and architectures, while reusability allows a piece of software, like a component or library, to be reused in different projects. Different approaches to both have emerged over time.
In the beginning, programming was very low level, as the only language available was machine code. This of course meant that such code was closely tied to the architecture or machine on which it was executed, and therefore could not be run on a different one without a rewrite or a port to another instruction set.
With the birth of high-level languages like C (high-level compared to assembly language or machine code), software portability increased. C, like any other programming language, comes with a standard library which, although small, contains the most important and common functions. Still, there are some challenges related to porting a C application from one platform to another due to inconsistent type sizes across platforms and other factors, but this was an important step forward.
Over time, other attempts to achieve software portability emerged, most notably the JVM (Java Virtual Machine) and .NET. Both are execution environments based on virtual machine technology that promised "write once, run anywhere." While they largely solved the portability problem within their ecosystems, they did not address reusability across different programming languages and runtimes.
Reusability Across Languages
When it comes to reusability across different programming languages, several approaches have emerged.
C Bindings and FFI
Almost all today's languages offer bindings to C as the lowest common layer. Through Foreign Function Interfaces (FFI), languages like Python, Ruby, Go or Rust can call C functions directly, making C libraries effectively universal. This is why so many critical libraries (OpenSSL, SQLite, zlib) are written in C — they can be consumed from virtually any language.
Interface Definition Languages
Another approach relies on Interface Definition Languages (IDLs) which describe data structures and service interfaces in a language-neutral way. Tools like Protocol Buffers (Protobuf) and FlatBuffers generate serialization and deserialization code for multiple target languages from a single schema definition. This allows different services, potentially written in different languages, to exchange data seamlessly. gRPC builds on top of Protobuf to extend this idea to remote procedure calls across language boundaries.
Language Transpilers
Language transpilers take yet another path — they allow writing code once and compiling it to multiple target languages. HAXE can transpile to C++, Java, JavaScript, C#, Python and several others from a single codebase. FusionLang follows a similar philosophy, enabling cross-language code generation from one source.
The problem with HAXE and FusionLang is that they are new languages, which might hinder their adoption.
Goany: Transpiling from an Existing Language
This is exactly the problem that led me to start goany two years ago — a hobby project I had been thinking about for a long time, aimed at answering a simple question: can I transpile a subset of Go to other programming languages and use it to write portable libraries in a single common language?
Since all valid goany programs are valid Go programs, developers can leverage existing Go tooling, ecosystem and knowledge while transpiling their code to C++, C#, Rust, JavaScript, or Java. By building on top of a well-established language rather than introducing a new one, goany avoids the adoption barrier that HAXE and FusionLang face. You may ask, why Go specifically? For a few reasons:
- It is a small, well-defined language
- Its standard library has everything you need
- It is a mainstream language with broad adoption
Beyond the choice of language, I had a few goals in mind: start with a small subset of Go and evolve it over time, produce reasonably good output code, explore optimization opportunities, and achieve nearly one-to-one correspondence between input and output.
Goany currently supports a focused subset of Go that is gradually evolving, balancing usability with simplicity of implementation. It covers primitives, slices, structs, functions with multiple returns, methods, loops, and conditionals — sufficient for writing reusable, cross-language libraries. It also provides cross-platform 2D graphics support, as demonstrated by a C64 emulator and a GUI demo, both written once in Go and running in the browser via JavaScript transpilation.
At the time I started, LLMs were not particularly helpful — I could use them occasionally to write selected functions, but they struggled with larger code bases, often generating incorrect or nonsensical code. Now, two years later, I'm impressed with the progress they have made. It is possible to generate large code bases, refactor, and iterate on ideas much faster, accelerating goany's development significantly — from generating transpilation rules to testing output across target languages.
Challenges and Trade-offs
Each of these approaches comes with its own set of challenges. FFI bindings must deal with mapping differences between languages, such as different memory management models (manual vs garbage collected), type system mismatches (e.g. nullable types, unsigned integers), and calling convention differences. IDL-based solutions add a serialization/deserialization overhead and require keeping schemas in sync. Transpilers, while powerful, may produce code that feels unidiomatic in the target language and can struggle with features unique to a specific language. Goany is no exception — its subset approach trades language completeness for cross-language compatibility, meaning developers must work within its supported feature set.
Looking Ahead
The quest for portable and reusable software is far from over. Each approach fills a different niche, and the right choice depends on the specific constraints of a project. Goany's bet is that a familiar language with a focused subset can strike a practical balance between expressiveness and cross-language reach — and that this balance will only improve as the subset grows and tooling matures.