DEV Community

Cover image for I Built Miruri: A Tool That Ports Existing C/C++ Projects Across CPUs and Operating Systems
Yuna
Yuna

Posted on

I Built Miruri: A Tool That Ports Existing C/C++ Projects Across CPUs and Operating Systems

CPUs and Operating Systems

I have been building a tool called Miruri.

The basic idea is simple:

Give it an existing software project and a target platform, and let it handle as much of the porting process as possible.

Repository:

https://github.com/yuna-r/miruri

It is still an alpha project, but it has reached the point where it can already do some surprisingly non-trivial things.

For example, I have used it to:

  • port Intel SSE code to ARM NEON
  • build Linux ARM64 binaries from an Apple Silicon Mac
  • build an Autotools terminal application for macOS
  • take a Linux/GNOME Python + GTK application and make it run as a macOS .app

That last one surprised me too.


What Miruri is trying to solve

Cross-compiling a clean, portable C project is usually not that difficult.

Real projects are different.

They contain things like:

x86 intrinsics
Windows APIs
Linux-specific APIs
architecture-specific compiler flags
platform GUI frameworks
graphics APIs
audio backends
plugins
generated files
custom build scripts
external SDK assumptions
Enter fullscreen mode Exit fullscreen mode

A compiler can tell you that something failed.

It usually cannot tell you how the architecture of the project should change to make another platform a first-class target.

Miruri tries to operate at that level.

Instead of thinking only in terms of:

source file
→ compiler
→ binary
Enter fullscreen mode Exit fullscreen mode

it treats porting more like:

existing project
      ↓
analyze platform assumptions
      ↓
construct target requirements
      ↓
choose a migration strategy
      ↓
modify an isolated copy
      ↓
build
      ↓
inspect artifacts
Enter fullscreen mode Exit fullscreen mode

Basic usage

Miruri itself is written in Go.

Building it is intentionally simple:

git clone https://github.com/yuna-r/miruri.git
cd miruri

go build -o bin/miruri ./cmd/miruri
Enter fullscreen mode Exit fullscreen mode

Check the local environment:

./bin/miruri doctor
Enter fullscreen mode Exit fullscreen mode

List targets:

./bin/miruri targets
Enter fullscreen mode Exit fullscreen mode

Then build a project for a target:

./bin/miruri build \
  --target macos-arm64 \
  path/to/project
Enter fullscreen mode Exit fullscreen mode

For projects that actually require source changes:

./bin/miruri port \
  --target macos-arm64 \
  path/to/project
Enter fullscreen mode Exit fullscreen mode

The port command allows broader source and build-system modifications.


It does not edit the original repository

One design decision I made early was that Miruri should not directly mutate the source repository being ported.

The flow is closer to:

original repository
        ↓
isolated working copy
        ↓
analysis and modifications
        ↓
build
        ↓
artifact collection
Enter fullscreen mode Exit fullscreen mode

This matters a lot once automated repair is involved.

Compiler output, generated files, build caches, object files, and experimental changes should not leak back into the original source tree.

Accepted source changes can instead be represented as a patch.


AI is a repair engine, not the source of truth

Miruri can use Codex CLI for source-level repair.

But I did not want the system to become:

paste compiler error into AI
→ trust whatever comes back
Enter fullscreen mode Exit fullscreen mode

The intended relationship is:

AI proposes changes

compiler validates syntax and ABI assumptions

linker validates symbol resolution

artifact inspection validates output architecture
Enter fullscreen mode Exit fullscreen mode

A typical flow is:

analyze
↓
build
↓
failure
↓
extract diagnostics
↓
repair isolated source
↓
build again
↓
inspect artifact
Enter fullscreen mode Exit fullscreen mode

For larger platform ports, the repair may involve more than changing a broken line.

It can add:

  • platform backends
  • compatibility code
  • conditional build logic
  • new entry points
  • GUI adapters
  • resource definitions

Linux sysroots are automatically provisioned

One annoying part of cross-compilation is building the target sysroot.

For example, compiling Linux ARM64 software from macOS may require:

headers
libc
crt objects
libgcc runtime
target libraries
multiarch paths
Enter fullscreen mode Exit fullscreen mode

Miruri can provision this automatically.

For example:

./bin/miruri build \
  --target linux-arm64 \
  path/to/project
Enter fullscreen mode Exit fullscreen mode

does not require manually supplying --sysroot.

Under the hood, Miruri can retrieve a matching OCI root filesystem and use it as build data.

It does not execute the container image.

The process is roughly:

OCI manifest
    ↓
select architecture
    ↓
download layers
    ↓
verify SHA-256 digests
    ↓
extract root filesystem
    ↓
validate toolchain/runtime files
    ↓
use as Clang sysroot
Enter fullscreen mode Exit fullscreen mode

No Docker daemon or QEMU is required for this part.

The result is cached by content digest.

You can also prefetch it:

./bin/miruri sysroot ensure \
  --target linux-arm64
Enter fullscreen mode Exit fullscreen mode

and inspect cached sysroots:

./bin/miruri sysroot list
Enter fullscreen mode Exit fullscreen mode

Offline builds can reuse them:

./bin/miruri build \
  --target linux-arm64 \
  --offline \
  path/to/project
Enter fullscreen mode Exit fullscreen mode

Build-system detection

Real-world OSS immediately forced me to support more than CMake.

Miruri currently understands:

CMake
Meson
Autotools
Make
Enter fullscreen mode Exit fullscreen mode

Examples:

CMakeLists.txt
→ CMake

meson.build
→ Meson

configure.ac / configure.in
→ Autotools
Enter fullscreen mode Exit fullscreen mode

For an Autotools Git checkout without a generated configure script, the flow can become:

configure.ac
↓
autoreconf
↓
configure
↓
make
Enter fullscreen mode Exit fullscreen mode

Meson can also be managed by Miruri when it is missing from the host.

This grew organically from testing actual open-source projects.


Real-world test 1: nudoku

One project I tested was nudoku, a terminal Sudoku application.

It uses Autotools.

At first Miruri simply stopped with:

detected: autotools
no supported build system
Enter fullscreen mode Exit fullscreen mode

So I added Autotools support.

After that, the same project could be built for Apple Silicon macOS.

That was the first point where Miruri started to feel less like a cross-compiler wrapper and more like a porting system.


Real-world test 2: a Linux GNOME GUI application on macOS

Then I tried something much more unreasonable.

I used Drawing, a Python + GTK drawing application designed for the GNOME/Linux desktop.

I ran:

./bin/miruri port \
  --target macos-arm64 \
  --codex-mode port \
  ~/src/drawing
Enter fullscreen mode Exit fullscreen mode

This exposed one missing feature after another.

The process looked roughly like this:

Meson project
↓
Miruri did not support Meson
↓
add Meson adapter

Meson missing on host
↓
add managed Meson runtime

project produces no native executable
↓
support interpreted install artifacts

Linux install tree is not a macOS app
↓
add .app packaging

Linux Python launcher assumptions fail on macOS
↓
add compatibility rewrites

PyGObject runtime not visible
↓
resolve Homebrew Python runtime

bundle entry point shadows Python package
↓
fix package resolution

GTK GSettings schemas not visible
↓
adjust macOS GTK runtime environment
Enter fullscreen mode Exit fullscreen mode

Eventually the Linux application opened on macOS as a real GUI application.

The GTK interface rendered correctly.

I could use:

  • menus
  • canvas drawing
  • text tools
  • fonts
  • checkboxes
  • radio buttons
  • Japanese UI
  • mouse interaction

Seeing a GNOME-oriented application show up as a macOS window after going through that pipeline was a fun moment.


Intel SSE to ARM NEON

Another experiment involved an intentionally x86-only C project.

The original source directly used:

#include <xmmintrin.h>
Enter fullscreen mode Exit fullscreen mode

and operations such as:

_mm_loadu_ps
_mm_add_ps
_mm_mul_ps
_mm_shuffle_ps
_mm_storeu_ps
Enter fullscreen mode Exit fullscreen mode

Miruri identified the architecture dependency and the resulting code gained separate backends:

#if defined(__SSE__)

    /* x86 */

#elif defined(__aarch64__)

    /* ARM NEON */

#else

    /* scalar */

#endif
Enter fullscreen mode Exit fullscreen mode

For example:

_mm_add_ps(...)
Enter fullscreen mode Exit fullscreen mode

became:

vaddq_f32(...)
Enter fullscreen mode Exit fullscreen mode

and:

_mm_loadu_ps(...)
Enter fullscreen mode Exit fullscreen mode

became:

vld1q_f32(...)
Enter fullscreen mode Exit fullscreen mode

The same source tree could then produce:

Linux ARM64
→ ELF AArch64

macOS ARM64
→ Mach-O arm64
Enter fullscreen mode Exit fullscreen mode

Compiler flags such as -msse also had to be made architecture-aware.

This is exactly the kind of issue Miruri is intended to find: architecture assumptions exist outside the C file too.


Project Graph and Target Contract

Internally, I am moving toward representing a project as capabilities rather than just files.

Conceptually:

Existing Project
      │
      ▼
Project Graph
      │
      ├── sources
      ├── build steps
      ├── resources
      ├── plugins
      ├── shaders
      └── platform capabilities
      │
      ▼
Target Contract
      │
      ▼
Strategy Planner
      │
      ├── native rebuild
      ├── source rewrite
      ├── compatibility layer
      ├── generated adapter
      └── unresolved blocker
      │
      ▼
Artifact Builder
Enter fullscreen mode Exit fullscreen mode

For example:

#include <xmmintrin.h>
Enter fullscreen mode Exit fullscreen mode

can be classified as a capability such as:

cpu.x86.intrinsics
Enter fullscreen mode Exit fullscreen mode

A Windows-only GUI dependency could similarly become:

platform.windows.gui
Enter fullscreen mode Exit fullscreen mode

This makes it possible to reason about a port at a higher level than individual compiler errors.


Artifact inspection matters

A successful compiler exit code is not enough.

Miruri inspects outputs such as:

ELF
Mach-O
PE
static archives
Enter fullscreen mode Exit fullscreen mode

and verifies their architecture.

If the target is:

macOS ARM64
Enter fullscreen mode Exit fullscreen mode

and the resulting Mach-O is actually x86_64, that should not be reported as a successful port.

Build results can also include metadata such as:

analysis.json
plan.json
build.log
manifest.json
sysroot.lock.json
Enter fullscreen mode Exit fullscreen mode

so that the generated artifact has some provenance.


Cross-target artifacts are not automatically executed

Miruri currently avoids automatically running foreign target binaries during the porting process.

For example, when creating an ARM64 Linux ELF on macOS, it stops at:

compile
↓
link
↓
artifact inspection
Enter fullscreen mode Exit fullscreen mode

instead of automatically launching it under an emulator.

That is intentional.

Once automatic execution is introduced, results can become dependent on:

  • emulator behavior
  • kernel behavior
  • drivers
  • graphics stack
  • audio stack
  • target services

I currently prefer to keep artifact production and target-runtime validation as separate stages.


What Miruri still cannot do

This is very much an alpha project.

There are many hard problems left.

Some examples:

Direct3D ↔ Vulkan ↔ Metal
shader translation
complex proprietary SDKs
driver dependencies
audio backend translation
full dependency resolution
semantic equivalence testing
bit-exact SIMD verification
production-quality standalone GUI packaging
Enter fullscreen mode Exit fullscreen mode

Automatically making something compile is much easier than proving that the new implementation behaves exactly like the old one.

That is especially true for numerical and SIMD-heavy software.


The development process has been surprisingly fun

One thing I have enjoyed is testing Miruri with real open-source projects instead of only synthetic fixtures.

A fixture tends to fail in the way you expected.

Real software fails in ways you did not think about.

The development history has looked something like:

CMake works
↓
try real Autotools project
↓
add Autotools

try Meson project
↓
add Meson

try interpreted GUI app
↓
add staged install artifacts

try Linux GTK application on macOS
↓
add macOS application packaging

try x86 intrinsics
↓
add architecture-level porting logic
Enter fullscreen mode Exit fullscreen mode

Each project reveals another assumption hidden inside the tool itself.

That has probably been the most useful form of testing.


Where I want to take it

Some areas I want to explore next:

SSE / AVX / AVX2 / AVX-512
→ NEON / SVE / SVE2

x86
→ RISC-V

x86
→ POWER

Windows GUI
→ macOS / Linux GUI

Direct3D
→ Vulkan / Metal
Enter fullscreen mode Exit fullscreen mode

I am also interested in making semantic validation a much larger part of the system.

The hard part of automated porting is not generating different code.

The hard part is establishing that the different code still means the same thing.


Summary

Miruri is an experiment in automating software porting across CPUs and operating systems.

Repository:

https://github.com/yuna-r/miruri

At the moment it has infrastructure for:

CMake
Meson
Autotools
Make

Linux
macOS
Windows

x86_64
ARM64
RISC-V
POWER
Enter fullscreen mode Exit fullscreen mode

and I have already used it for experiments including:

Intel SSE
→ ARM NEON
Enter fullscreen mode Exit fullscreen mode
Autotools application
→ Apple Silicon macOS
Enter fullscreen mode Exit fullscreen mode

and the slightly ridiculous one:

Linux/GNOME Python + GTK application
→ macOS .app
→ working GUI
Enter fullscreen mode Exit fullscreen mode

There is still a lot to build, but the main question behind the project is simple:

How much of real software porting can we turn into a repeatable automated process?

That is what I am trying to find out.

Top comments (0)