Bemo adds native I/O, Rustls TLS with AWS-LC, and zlib-rs compression to Netty. It runs on the JVM through FFM or links statically into GraalVM Native Image. Your application keeps its Netty pipelines and handlers. Framework HTTP codecs stay in Java; the native HTTP server path is a separate integration.
Bemo 0.3.0 is on Maven Central. For a Netty application on JDK 22+, add
the dependencies below and run with --enable-native-access=ALL-UNNAMED.
Qualified packages target Linux x86-64 / glibc 2.39 and macOS 15 / ARM64.
Netty TLS requires the classpath. Windows, musl/Alpine, Linux ARM64, and Intel
macOS packages are not qualified. The snippets use Linux; on macOS ARM64, replace
linux-x86_64-gnu with osx-aarch64.
Gradle (build.gradle.kts)
repositories { mavenCentral() }
dependencies {
implementation("dev.elide.bemo:bemo-netty:0.3.0")
implementation("dev.elide.bemo:bemo-ffm:0.3.0")
runtimeOnly("dev.elide.bemo:bemo-ffm:0.3.0:linux-x86_64-gnu")
}Maven (pom.xml)
Maven uses Central by default. Add these dependencies inside <project>:
<properties>
<bemo.version>0.3.0</bemo.version>
</properties>
<dependencies>
<dependency>
<groupId>dev.elide.bemo</groupId>
<artifactId>bemo-netty</artifactId>
<version>${bemo.version}</version>
</dependency>
<dependency>
<groupId>dev.elide.bemo</groupId>
<artifactId>bemo-ffm</artifactId>
<version>${bemo.version}</version>
</dependency>
<dependency>
<groupId>dev.elide.bemo</groupId>
<artifactId>bemo-ffm</artifactId>
<version>${bemo.version}</version>
<classifier>linux-x86_64-gnu</classifier>
<scope>runtime</scope>
</dependency>
</dependencies>Gradle version catalog
In gradle/libs.versions.toml:
[versions]
bemo = "0.3.0"
[libraries]
bemo-netty = { module = "dev.elide.bemo:bemo-netty", version.ref = "bemo" }
bemo-ffm = { module = "dev.elide.bemo:bemo-ffm", version.ref = "bemo" }In build.gradle.kts:
repositories { mavenCentral() }
dependencies {
implementation(libs.bemo.netty)
implementation(libs.bemo.ffm)
runtimeOnly(variantOf(libs.bemo.ffm) { classifier("linux-x86_64-gnu") })
}The classifier belongs in the build script; version catalogs store the module and version.
Bemo loads the native library from the classifier JAR automatically. See the
package guide for platform requirements and Native Image
linking. Start the complete Maven or Gradle quickstart
without building Bemo. It returns Hello, World! at
http://127.0.0.1:8080/plaintext. The framework examples
are separate source-build examples.
This integration fragment creates an event-loop group for a ServerBootstrap:
the quickstart supplies the complete runnable server.
import dev.elide.bemo.transport.FfmTransportNative;
import dev.elide.bemo.transport.NativeIoHandler;
import dev.elide.bemo.transport.NativeServerSocketChannel;
import dev.elide.bemo.transport.Workload;
import io.netty.channel.MultiThreadIoEventLoopGroup;
var transport = new FfmTransportNative();
var group = new MultiThreadIoEventLoopGroup(
2, NativeIoHandler.newFactory(transport, 0, 128, 8 * 1024 * 1024));
// Supply group and NativeServerSocketChannel.class to your ServerBootstrap.
// After closing your channels, release the event loops and default workload:
group.shutdownGracefully().sync();
Workload.close(transport, Workload.DEFAULT);For Native Image, use dev.elide.bemo.transport.svm.CapiTransportNative and
the static library from the bemo-native-image platform classifier. See the
static linking instructions.
The transport examples cover TCP, Unix sockets, and TLS, including setup and shutdown. Netty TLS currently requires the classpath rather than JPMS. For library paths and extraction settings, see native loading.
To use the Rust core directly, pin a full commit SHA:
[dependencies]
bemo = { git = "https://github.com/elide-dev/bemo", rev = "<full-commit-sha>" }For C exports, add bemo-ffi at the same revision. Public headers live in
include; the bemo crate itself has no JVM or Netty dependency.
Fresh development benchmarks measure
16805b3 plus the recorded benchmark harness patch, 2026-10-09 UTC,
on a Linux Threadripper PRO 9965WX. This revision follows release 0.3.0;
these are development results. Benchmark builds use Rust target-cpu=native
and Native Image -O3 -march=native. Release build defaults remain portable;
downloaded Netty JNI libraries retain their published CPU targets.
The full-stack comparison uses Bemo Native Image / native HTTP / io_uring / Rustls against OpenJDK 25.0.2 / Netty HTTP / epoll / tcnative BoringSSL. It includes runtime and codec differences. Native Image is GraalVM 25.3.4.1+1.1. Large uncompressed TLS throughput is near parity (β0.6% and β0.1%).
The framework matrix measures the same 16805b3 snapshot, 2026-10-09 UTC,
with Spring Boot, Micronaut, and Ktor on OpenJDK 25.0.2 and GraalVM Native Image
-O3 -march=native. Runtime and framework HTTP codecs stay fixed within each
pair. Compression compares reusable zlib-rs with the reusable JDK Deflater
helper at level 1:
Across gzip and TLS+gzip, throughput was 2.2β3.0Γ the stock JVM framework throughput and 4.8β7.5Γ the stock Native Image throughput. Large uncompressed plaintext JVM responses were 13β17% slower; Native Image Micronaut was 10% slower there. Both sides used gzip level 1, but Bemo produced larger compressed bodies: 1,580 vs. 909 bytes for the 128 KiB framework payload. Gains cannot be attributed only to transport.
These are closed-loop loopback measurements on a shared host, with three samples per workload; ranges show observed minima and maxima, not confidence intervals. The report includes latency, CPU, memory, raw samples, source hashes, and reproduction commands. Earlier measurements remain archived.
Install Rustup, Python 3.11+, a C toolchain, CMake, a JDK 25 build toolchain,
and the Elide version in .elide-version. Rustup selects
the pinned Rust toolchain automatically. Native Image tests need GraalVM
JDK 25 with native-image.
make deps
make build
make check
make test
make test-native-imageCargo builds Rust; Elide compiles Java and builds the JARs. make test covers
Rust, a revision-pinned Cargo consumer, a C consumer, and the JVM FFM binding.
make test-native-image runs the shared Java contract through the static binding.
Run build commands sequentially because they share output directories.
Use make package and python3 tools/verify_package.py to stage and check
packages locally. Set ELIDE to choose the build executable, JAVA_HOME for
JVM tools, or BEMO_TEST_JAVA to test with another JVM. Windows builds also
need NASM or AWS_LC_SYS_PREBUILT_NASM=1; OpenSSL on PATH enables additional
TLS interoperability checks. Both commands also stage or verify a ThinLTO
bitcode classifier, which needs LLVM_BIN set to a pinned LLVM/Clang/LLD
toolchain (python3 tools/setup_llvm.py installs it); see
generated seams for generation, extraction, and
final-link requirements.
Source and documentation:
crates/bemo: Rust core.crates/bemo-ffiandinclude: C interface.packages: Java API, FFM, Native Image, and Netty adapters.- Architecture and I/O ownership: how the pieces fit together.
- Measurement and native safety: benchmarks, coverage, sanitizers, and fuzzing.
Bemo is the main transport for Elide. The name comes from the minibuses that carry people around Indonesia, including Bali.
Apache-2.0.
