Alloy · a Gradle toolchain

Two materials.
One build.

Java on one side. C and C++ on the other. Alloy fuses them into a single reproducible Gradle build — generating the JNI bridges, compiling the natives for every target, and packaging the binaries into your jars.

Pre-release · in active development · not yet published to Maven Central

The overview

The boundary is the hard part. Alloy owns the boundary.

Everything painful about mixing Java and native code lives at the seam: writing JNI by hand, keeping signatures in sync, cross-compiling for each platform, shipping the right binary to the right machine, and loading it at runtime without surprises. Alloy takes that seam as its job — you write the two halves, it makes them one artifact.

./gradlew build

Compiles your C/C++ next to your Java, links per target and build type, and packages each platform's binary into a classified jar.

no hand-written JNI

Bridges are generated from annotations — including a C++ wrapper layer where Java objects, fields and methods are typed C++ values.

runs everywhere you ship

macOS, Linux and Android NDK targets from one build, with Conan dependencies and IDE integration resolved per platform.

The bridges

Five ways across, one build file.

Each of these is a different answer to the same question — how should this particular piece of code cross the boundary? Pick per method, not per project.

01 Write C++ that speaks Java

Annotate a class and Alloy generates Hello.hpp — a typed C++ class where Java fields are assignable members and Java methods are callable members. You implement the native methods against it; exceptions propagate both ways.

JavaHello.java
import build.native.annotations.NativeClass;
import build.native.annotations.NativeMethod;
import build.native.annotations.NativeField;
import build.native.runtime.Alloy;

@NativeClass
public class Hello {
    static final NativeLibrary LIB =
        Alloy.loadLibrary(Hello.class);

    /** Visible to C++ as JavaField<jint>. */
    @NativeField
    int greetCount = 0;

    /** Implemented in Hello.cpp. */
    @NativeMethod
    public native String greet(String name, int n);

    /** Callable from C++ as a JavaMethod. */
    @NativeMethod
    public String repeat(String text, int times) {
        return text.repeat(times);
    }
}
C++
#include "com/example/Hello.hpp"

using namespace alloy;
using namespace java::lang;

Local<String> com::example::Hello::greet(
        const Reference<String>& name, jint n) {

    // Call back into Java — no receiver needed.
    auto repeated = this->repeat(name, n);

    auto sb = StringBuilder::create();
    sb->append("Hello [");
    sb->appendString(repeated);
    sb->append("] from C++!");

    greetCount++;      // a Java field, from C++

    return sb->toString();
}
/* Generated by Alloy — do not edit */
#pragma once
#include <jni.hpp>

namespace com::example {

using namespace alloy;
using namespace java::lang;

class Hello : public Object {
public:
    static constexpr char className[]
        = "com/example/Hello";

    JavaField<jint> greetCount{"greetCount"};

    JavaMethod<String(String, jint)> repeat{"repeat"};

    Local<String> greet(const Reference<String>& name,
                        jint n);
};

}

02 Or write no native code at all

Mark a native override with @TranspileNative and Alloy transpiles the superclass's Java implementation into C, binds it to the override, and keeps the original method intact and callable.

JavaComputations.java
public class Computations {

    public boolean isPrime(long n) {
        if (n < 2) return false;
        for (long d = 2; d * d <= n; d++)
            if (n % d == 0) return false;
        return true;
    }
}

public class Fast extends Computations {

    @Override
    @TranspileNative
    public native boolean isPrime(long n);
}
generated CFast.c
JNIEXPORT jboolean JNICALL
Java_com_example_Fast_isPrime(JNIEnv *env,
                              jobject self, jlong n) {

    if (n < 2) return JNI_FALSE;

    for (jlong d = 2; alloy_lmul(d, d) <= n; d++)
        if (alloy_lrem(n, d) == 0) return JNI_FALSE;

    return JNI_TRUE;
}

/* Java semantics kept exactly: wrap-on-overflow
   arithmetic, MIN/-1 division guards, NaN-correct
   comparisons. Not "C that looks similar". */

03 Structs, without the copy

Declare a C layout as a Java interface. Alloy generates the accessors over a direct buffer, so native code receives a real pointer to the same bytes — no marshalling, no copies, mutations visible on both sides.

JavaSample.java
public interface Sample extends Struct<Sample> {

    @Field int id();
    Sample id(int v);

    @CString(12) String name();
    Sample name(String s);

    @Field @Array(3) float velocity(int i);
    Sample velocity(int i, float v);

    @Field Value value();      // an embedded union
}

// Allocate one, or map the layout onto bytes
// you already have — a slice of a ByteBuffer,
// a mapped file, a shared arena:

Sample s = Struct.of(Sample.class).allocateDirect();
Sample view = Struct.of(Sample.class).map(buffer);

s.id(7).name("probe").velocity(0, 9.81f);

// Hand it straight to native code — no marshalling:
new Probe().stamp(s, 0.016f);
C++Sample.h · Probe.cpp
struct Sample {
    int32_t     id;           /* @0  */
    char        name[12];     /* @4  */
    float       velocity[3];  /* @16 */
    union Value value;        /* @28 */
};                            /* size 32, align 4 */

/* Java hands over a direct ByteBuffer. The bridge
   resolves it with GetDirectBufferAddress, so your
   method is called with a real Sample* — pointing
   at the very bytes the Java object reads. */

void com::example::Probe::stamp(Sample *s, jfloat dt) {

    s->id += 1;
    s->velocity[1] -= 9.81f * dt;
    strncpy(s->name, "probe", sizeof s->name);
}

/* No copy in, no copy out. Every write is visible
   from Java the moment it lands. */

04 SIMD, from ordinary Java source

Add vectorize = true and the transpiler emits vector-extension C for recognised reduction and elementwise patterns. @Critical pins the arrays so no copy happens on the way in.

JavaVectorKernels.java
@TranspileNative(vectorize = true)
public static double dotProduct(@Critical double[] a,
                                @Critical double[] b,
                                int len) {
    double sum = 0.0;
    for (int i = 0; i < len; i++)
        sum += a[i] * b[i];
    return sum;
}
generated CVectorKernels.c
typedef double v4d __attribute__((vector_size(32)));

v4d acc = {0.0, 0.0, 0.0, 0.0};
jint i = 0;

for (; i + 4 <= len; i += 4)
    acc += *(v4d *)(a + i) * *(v4d *)(b + i);

jdouble sum = acc[0] + acc[1] + acc[2] + acc[3];

for (; i < len; i++)          /* scalar tail */
    sum += a[i] * b[i];

05 Native code that cannot take the JVM with it

One annotation moves a method into a child process, connected by a Unix socket with a shared memory arena. Structs cross as descriptors, not copies — and a segfault surfaces as an exception instead of killing your JVM.

JavaKernels.java
public class Kernels {

    @NativeMethod
    @IsolateNative("mathCompute")
    public native void accumulate(Counters c);
}

// A crash in the child throws
// IsolationCrashException. A 64 KB struct never
// crosses the socket — only its
// (segment, offset, size) descriptor does.
runtimeisolation
  JVM  ──unix socket──▶  alloy-isolation-driver
   │                            │
   └────── shared arena ────────┘
           (mmap, zero-copy)

/* The generated bridge runs unmodified in the
   child. The parent keeps running, whatever
   happens over there. */

The build

One build file describes every platform you ship.

Targets, build types, cross-compilation and native dependencies are ordinary Gradle configuration. Each target produces its own archive, its own linked library and its own classified jar — from a single ./gradlew build.

Gradlebuild.gradle.kts
plugins {
    java
    id("build.native.cpp")
    id("build.native.jni")
}

alloy {
    cpp { standard.set(CppStandard.CPP20) }

    buildTypes {
        debug { }
        release {
            cpp { flags += "-O2" }
            linker { flags += "-s" }
        }
    }

    targets {
        create("host") {
            buildType("release")
            packaging { jar { classifier(host) } }
        }
        create("android") {
            conan { profile("android-arm64") }
            packaging { jar { classifier(name) } }
        }
    }

    conan {
        conanfile {
            requires("eigen/3.4.0")
            requires("zlib/1.3.1")
        }
    }

    ide {
        clangd { compileCommands(true) }  // real C++ completion
        make   { makefile(true) }         // a real orchestrator
    }
}

The scope

What else is in the alloy.

Generated JNI

Bridges from annotations, with correct name mangling, exception checks and null handling.

C++ object framework

Java references, arrays, strings and fields as typed C++ values with scoped lifetimes.

Java → C transpiler

Bytecode to C with Java semantics preserved, including a build-failing check when emission can't be trusted.

Vectorization

SIMD C for recognised loop patterns, with alignment honoured only where it is actually guaranteed.

Zero-copy structs

C layouts as Java interfaces — arrays, strings, unions, explicit alignment, shared arenas.

Process isolation

Per-group child processes over Unix sockets; crashes become exceptions, not core dumps.

Cross-compilation

macOS, Linux and Android NDK targets, each with its own toolchain, flags and packaging.

Conan dependencies

Native libraries resolved per target, with pkg-config flags folded into compile and link.

Make & Ninja

Generated build files that are both IDE artifacts and real orchestrators for the native step.

Native tests

Test executables built and run per build type, alongside your JVM tests.

Runtime loading

Per-platform binaries extracted from the jar into owner-only temp storage and loaded on demand.

IDE integration

compile_commands.json for clangd, so the C++ half is as navigable as the Java half.

Still being forged.

Alloy is in active development ahead of its first public release. The build, the bridges and the demos all run today; the coordinates below are what they will be published as.

runtimebuild.native:struct
pluginbuild.native.cpp