Skip to content
This repository was archived by the owner on Sep 11, 2026. It is now read-only.
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/gradle.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: Build

on:
push:
branches: [ '*' ]
branches: [ main ]
tags: [ '*' ]
pull_request:
branches: [ main ]
Expand Down
41 changes: 41 additions & 0 deletions .github/workflows/native-regressions.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: Native transport regressions

on:
push:
branches: [ nxs-dev ]
pull_request:
branches: [ upstream, nxs-dev ]
workflow_dispatch:

permissions:
contents: read

jobs:
native-regressions:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
submodules: recursive
- uses: actions/setup-java@v5
with:
java-version: '17'
distribution: temurin
cache: gradle
- uses: gradle/actions/setup-gradle@v5
- name: Install native build dependencies
run: sudo apt-get update && sudo apt-get install -y cmake build-essential libssl-dev
- name: Native and JVM regressions
run: |
./gradlew :nativeTransportProbe :test --no-daemon --max-workers=2 --no-configuration-cache \
-Plibdatachannel.java-compiler-version=17 \
-Plibdatachannel.test-native-path="$PWD/build/native-probe/libdatachannel-java.so"
- uses: actions/upload-artifact@v6
if: always()
with:
name: native-regression-reports
path: |
build/test-results/
build/reports/tests/
build/native-probe/libdatachannel/Testing/
1 change: 1 addition & 0 deletions .github/workflows/publish-web.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ on:

jobs:
publish-web:
if: github.repository == 'pschichtel/libdatachannel-java'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/readme-version.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ on:

jobs:
update:
if: github.repository == 'pschichtel/libdatachannel-java'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
Expand Down
3 changes: 1 addition & 2 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
[submodule "jni/libdatachannel"]
path = jni/libdatachannel
url = https://github.com/paullouisageneau/libdatachannel.git
branch = v0.24.1
url = https://github.com/teamziax/libdatachannel.git
[submodule "jni/cmake-conan"]
path = jni/cmake-conan
url = https://github.com/conan-io/cmake-conan.git
90 changes: 87 additions & 3 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ fun produceVersion(): String {
}
}

version = produceVersion()
version = providers.gradleProperty("libdatachannel.development-version").getOrElse(produceVersion())
val isSnapshot = version.toString().endsWith("-SNAPSHOT")
description = "${project.name} is a binding to the libdatachannel that feels native to Java developers."

Expand Down Expand Up @@ -343,7 +343,22 @@ dependencies {
annotationProcessor(libs.jniAccessGenerator)
compileOnly(libs.jniAccessGenerator)

testImplementation(files(packageNativeForHost))
if (providers.gradleProperty("libdatachannel.test-native-path").isPresent) {
// Package the selected binary on the classpath so child JVM lifecycle probes
// load the same checkout's library without relying on inherited properties.
val focusedNative = tasks.register<Jar>("packageNativeForFocusedTests") {
dependsOn("compileNativeProbe")
archiveFileName = "focused-test-native.jar"
destinationDirectory = layout.buildDirectory.dir("focused-test-native")
from(providers.gradleProperty("libdatachannel.test-native-path")) {
into("native")
rename { "libdatachannel-java.so" }
}
}
testImplementation(files(focusedNative))
} else {
testImplementation(files(packageNativeForHost))
}
}

publishing.publications.withType<MavenPublication>().configureEach {
Expand Down Expand Up @@ -424,11 +439,80 @@ val githubActions by tasks.registering(DefaultTask::class) {
val deployRefPattern = """^refs/(?:tags/v\d+\.\d+\.\d+\.\d+|heads/main)$""".toRegex()
val ref = System.getenv("GITHUB_REF")?.ifBlank { null }?.trim()

if (ref != null && deployRefPattern.matches(ref)) {
if (System.getenv("GITHUB_REPOSITORY") == "pschichtel/libdatachannel-java" && ref != null && deployRefPattern.matches(ref)) {
logger.lifecycle("Job in $ref will deploy!")
dependsOn(mavenCentralDeploy)
} else {
logger.lifecycle("Job will only build!")
dependsOn(tasks.assemble)
}
}

// Focused local JNI transport tests using the existing library/package format.
// Uses system OpenSSL; the established dockcross release path remains available.
val configureNativeProbe by tasks.registering(Exec::class) {
dependsOn(tasks.compileJava)
commandLine("cmake", "-S", "jni", "-B", "build/native-probe", "-DCMAKE_POLICY_VERSION_MINIMUM=3.5",
"-DLIBDATACHANNEL_SOURCE_DIR=${project.file("jni/libdatachannel").absolutePath}", "-DUSE_SYSTEM_JUICE=OFF",
"-DCMAKE_BUILD_TYPE=Debug", "-DPROJECT_VERSION=${project.version}", "-DENABLE_LOCALHOST_ADDRESS=ON",
"-DTRANSPORT_TEARDOWN_TESTS=ON", "-DPENDING_MUX_TESTS=ON", "-DICE_UDP_MUX_TESTS=ON",
"-DRTC_ENABLE_TEST_DIAGNOSTICS=ON")
}
val compileNativeProbe by tasks.registering(Exec::class) {
dependsOn(configureNativeProbe)
commandLine("cmake", "--build", "build/native-probe", "--target", "datachannel-java", "transport-teardown-test", "ice-udp-mux-pending-test", "mux-pending-test", "mux-pending-lifetime-test", "mux-authentication-test", "ice-attribute-limits-test", "-j2")
}
val probeSourceSet = sourceSets.create("nativeProbe") {
java.srcDir("native-test")
compileClasspath += sourceSets.main.get().output + configurations.compileClasspath.get()
runtimeClasspath += sourceSets.main.get().output + configurations.runtimeClasspath.get()
}
dependencies {
add(probeSourceSet.implementationConfigurationName, libs.logbackClassic)
}
tasks.named<JavaCompile>(probeSourceSet.compileJavaTaskName) {
javaCompiler = javaToolchains.compilerFor { languageVersion = JavaLanguageVersion.of(17) }
options.release = 17
}
val probeIdentity by tasks.registering(Exec::class) {
val dir = layout.buildDirectory.dir("probe-identity")
outputs.dir(dir)
doFirst { dir.get().asFile.mkdirs() }
commandLine("openssl", "req", "-x509", "-newkey", "ec", "-pkeyopt", "ec_paramgen_curve:prime256v1",
"-nodes", "-days", "1", "-subj", "/CN=native-probe", "-keyout", "build/probe-identity/key.pem", "-out", "build/probe-identity/cert.pem")
}
val probeEncryptedIdentity by tasks.registering(Exec::class) {
dependsOn(probeIdentity)
inputs.file("build/probe-identity/key.pem")
outputs.file("build/probe-identity/key-encrypted.pem")
commandLine("openssl", "pkcs8", "-topk8", "-in", "build/probe-identity/key.pem",
"-out", "build/probe-identity/key-encrypted.pem", "-v2", "aes-256-cbc", "-passout", "pass:test-only-password")
}
val runTransportNativeTests by tasks.registering(Exec::class) {
dependsOn(compileNativeProbe)
commandLine("ctest", "--test-dir", "build/native-probe/libdatachannel", "--output-on-failure", "-R", "transport.teardown|mux.pending|mux.authentication|ice.attribute.limits")
}
tasks.register<JavaExec>("nativeTransportProbe") {
dependsOn(runTransportNativeTests, probeIdentity, probeEncryptedIdentity, tasks.named(probeSourceSet.classesTaskName), "nativeCallbackCleanupProbe", "nativeLoggingProbe")
javaLauncher = javaToolchains.launcherFor { languageVersion = JavaLanguageVersion.of(17) }
classpath = probeSourceSet.runtimeClasspath
mainClass = "tel.schich.libdatachannel.NativeTransportProbe"
systemProperty("libdatachannel.native.datachannel-java.path", layout.buildDirectory.file("native-probe/libdatachannel-java.so").get().asFile.absolutePath)
args("build/probe-identity/cert.pem", "build/probe-identity/key.pem", "build/probe-identity/key-encrypted.pem")
}

tasks.register<JavaExec>("nativeCallbackCleanupProbe") {
dependsOn(compileNativeProbe, tasks.named(probeSourceSet.classesTaskName))
javaLauncher = javaToolchains.launcherFor { languageVersion = JavaLanguageVersion.of(17) }
classpath = probeSourceSet.runtimeClasspath
mainClass = "tel.schich.libdatachannel.CallbackCleanupProbe"
systemProperty("libdatachannel.native.datachannel-java.path", layout.buildDirectory.file("native-probe/libdatachannel-java.so").get().asFile.absolutePath)
}

tasks.register<JavaExec>("nativeLoggingProbe") {
dependsOn(compileNativeProbe, tasks.named(probeSourceSet.classesTaskName))
javaLauncher = javaToolchains.launcherFor { languageVersion = JavaLanguageVersion.of(17) }
classpath = probeSourceSet.runtimeClasspath
mainClass = "tel.schich.libdatachannel.NativeLoggingProbe"
systemProperty("libdatachannel.native.datachannel-java.path", layout.buildDirectory.file("native-probe/libdatachannel-java.so").get().asFile.absolutePath)
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@ tasks.test {
}

tasks.compileJava {
options.release = 11
javaCompiler = javaToolchains.compilerFor {
languageVersion = JavaLanguageVersion.of(11)
languageVersion = JavaLanguageVersion.of(providers.gradleProperty("libdatachannel.java-compiler-version").getOrElse("11").toInt())
}
}

Expand Down
18 changes: 18 additions & 0 deletions docs/contribution-provenance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Contribution provenance

This maintained fork compares `nxs-dev` with the upstream mirror at `b31809f14e0d8d62d4c800902e03b4108d95529d`.
The aggregate owned draft records the combined work; future external submissions
should separate independent fixes, acceptance APIs and lifecycle changes.

| Source | Contribution |
| --- | --- |
| Upstream [`b31809f1`](https://github.com/pschichtel/libdatachannel-java/commit/b31809f14e0d8d62d4c800902e03b4108d95529d) | Baseline with thread attachment, unload synchronization, callback lifetime fixes and regression tests. |
| Zulu `39ec8c6`, `70efb59` | Original identity, ICE, packet-callback and local packaging work. Application-specific probes moved downstream with attribution. |
| Zulu `0812c7e`, `5544964`, `7885652` | Cleanup completion, callback-context checks and callback detachment. |
| Zulu `4a12f67`, `40f2c32` | Earlier deferred packet handling and limits, now replaced by native-owned pending requests. |
| Merge commits `97268df`, `66014e1` | History only; no duplicate patches. |
| Consolidation and current changes | Public C API use, asynchronous metadata callbacks, retained failure ownership, native log filtering, encrypted PEM support and generic regressions. |

## Review follow-up, 8 September 2026

Settle waiting-request cancellation independently of the application executor, bind authenticated peer reuse and asynchronous destruction, add named statistics and an acceptance builder with shared identity configuration, retain compatibility adapters, and move construction diagnostics out of the public Java API.
9 changes: 7 additions & 2 deletions jni/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ set(NO_WEBSOCKET ON CACHE BOOL "configure libdatachannel build")
set(NO_MEDIA ON CACHE BOOL "configure libdatachannel build")
set(NO_TESTS ON CACHE BOOL "configure libdatachannel build")
set(NO_EXAMPLES ON CACHE BOOL "configure libdatachannel build")
add_subdirectory(libdatachannel)
set(LIBDATACHANNEL_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/libdatachannel" CACHE PATH "libdatachannel source checkout")
add_subdirectory(${LIBDATACHANNEL_SOURCE_DIR} libdatachannel)

include_directories(libdatachannel/include generated)
include_directories(${LIBDATACHANNEL_SOURCE_DIR}/include generated)
include_directories(jdk)
if(WIN32)
include_directories(jdk/windows)
Expand Down Expand Up @@ -54,6 +55,10 @@ add_library(datachannel-java SHARED
src/util.c
src/native_channel.c
src/native_peer.c
src/native_mux.c
src/native_track.c
src/callback.c)
target_link_libraries(datachannel-java PRIVATE datachannel-static)
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
target_link_options(datachannel-java PRIVATE -Wl,--no-undefined)
endif()
2 changes: 1 addition & 1 deletion jni/libdatachannel
Submodule libdatachannel updated 129 files
9 changes: 8 additions & 1 deletion jni/src/init.c
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,18 @@ JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* jvm, void* reserved) {
global_JVM = jvm;
JNIEnv* env = get_jni_env_from_jvm(jvm);
module_OnLoad(env);
rtcInitLogger(RTC_LOG_VERBOSE, &logger_callback);
rtcLogLevel level = (rtcLogLevel)call_tel_schich_libdatachannel_LibDataChannel_initialNativeLogLevel(env);
if ((*env)->ExceptionCheck(env)) return JNI_ERR;
rtcInitLogger(level, &logger_callback);
rtcPreload();
return JNI_VERSION;
}

JNIEXPORT void JNICALL Java_tel_schich_libdatachannel_LibDataChannel_setLogLevelNative(
JNIEnv* env, jclass clazz, jint level) {
rtcInitLogger((rtcLogLevel)level, &logger_callback);
}

JNIEXPORT void JNICALL JNI_OnUnload(JavaVM* jvm, void* reserved) {
rtcCleanup();
JNIEnv* env = get_jni_env();
Expand Down
97 changes: 97 additions & 0 deletions jni/src/native_mux.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
#include "util.h"
#include <jni.h>
#include <rtc/rtc.h>
#include <stdint.h>
#include <stdlib.h>

struct ice_mux {
int listener;
jobject owner;
jmethodID dispatch;
};

JNIEXPORT jint JNICALL Java_tel_schich_libdatachannel_IceUdpMuxListener_listenerIdNative(
JNIEnv *env, jclass clazz, jlong handle) {
return ((struct ice_mux *)(intptr_t)handle)->listener;
}

static void RTC_API incoming_request(int listener, const rtcIceUdpMuxRequest *request, void *ptr) {
struct ice_mux *mux = ptr;
JNIEnv *env = get_jni_env();
bool queued = false;
if (env && (*env)->PushLocalFrame(env, 4) == 0) {
jstring local = (*env)->NewStringUTF(env, request->localUfrag);
jstring remote = !(*env)->ExceptionCheck(env) ? (*env)->NewStringUTF(env, request->remoteUfrag) : NULL;
jstring address = !(*env)->ExceptionCheck(env) ? (*env)->NewStringUTF(env, request->remoteAddress) : NULL;
if (!(*env)->ExceptionCheck(env)) queued = (*env)->CallBooleanMethod(env, mux->owner,
mux->dispatch, (jlong)request->id, local, remote, address, (jint)request->remotePort);
if ((*env)->ExceptionCheck(env)) { (*env)->ExceptionClear(env); queued = false; }
(*env)->PopLocalFrame(env, NULL);
} else if (env && (*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env);
if (!queued) rtcRejectIceUdpMuxRequest(listener, request->id);
}

JNIEXPORT jlong JNICALL Java_tel_schich_libdatachannel_IceUdpMuxListener_openNative(
JNIEnv *env, jobject self, jstring address, jint port, jint maxPending, jint timeoutMs) {
struct ice_mux *mux = calloc(1, sizeof(*mux));
if (!mux) return 0;
mux->listener = -1;
mux->owner = (*env)->NewGlobalRef(env, self);
jclass clazz = !(*env)->ExceptionCheck(env) ? (*env)->GetObjectClass(env, self) : NULL;
mux->dispatch = clazz ? (*env)->GetMethodID(env, clazz, "dispatch", "(JLjava/lang/String;Ljava/lang/String;Ljava/lang/String;I)Z") : NULL;
if (clazz) (*env)->DeleteLocalRef(env, clazz);
const char *host = !(*env)->ExceptionCheck(env) ? (*env)->GetStringUTFChars(env, address, NULL) : NULL;
if (host && mux->owner && mux->dispatch) {
rtcIceUdpMuxListenerConfiguration config = {.bindAddress = host, .port = (uint16_t)port,
.maxPendingRequests = (unsigned int)maxPending, .requestTimeoutMs = (unsigned int)timeoutMs};
mux->listener = rtcCreateIceUdpMuxListener(&config, incoming_request, mux);
}
if (host) (*env)->ReleaseStringUTFChars(env, address, host);
if (mux->listener < 0) {
if (mux->owner) (*env)->DeleteGlobalRef(env, mux->owner);
free(mux);
return 0;
}
return (jlong)(intptr_t)mux;
}

JNIEXPORT void JNICALL Java_tel_schich_libdatachannel_IceUdpMuxListener_closeNative(
JNIEnv *env, jclass clazz, jlong handle) {
struct ice_mux *mux = (struct ice_mux *)(intptr_t)handle;
if (rtcDeleteIceUdpMuxListener(mux->listener) != RTC_ERR_SUCCESS) {
throw_native_exception(env, "Failed to close ICE UDP mux listener");
return;
}
// Native deletion waits for in-flight metadata callbacks before releasing this reference.
(*env)->DeleteGlobalRef(env, mux->owner);
free(mux);
}

JNIEXPORT jint JNICALL Java_tel_schich_libdatachannel_IceUdpMuxListener_acceptNative(
JNIEnv *env, jclass clazz, jint listener, jlong requestId, jint peer) {
return rtcAcceptIceUdpMuxPeer(listener, (uint64_t)requestId, peer);
}

JNIEXPORT jint JNICALL Java_tel_schich_libdatachannel_IceUdpMuxListener_rejectNative(
JNIEnv *env, jclass clazz, jint listener, jlong requestId) {
return rtcRejectIceUdpMuxRequest(listener, (uint64_t)requestId);
}

JNIEXPORT jint JNICALL Java_tel_schich_libdatachannel_IceUdpMuxListener_attachNative(
JNIEnv *env, jclass clazz, jint listener, jlong requestId, jint peer) {
return rtcAttachIceUdpMuxPeer(listener, (uint64_t)requestId, peer);
}

JNIEXPORT jlongArray JNICALL Java_tel_schich_libdatachannel_IceUdpMuxListener_statsNative(
JNIEnv *env, jclass clazz, jint listener) {
rtcIceUdpMuxListenerStats stats;
if (rtcGetIceUdpMuxListenerStats(listener, &stats) != RTC_ERR_SUCCESS) {
throw_native_exception(env, "ICE UDP mux statistics unavailable");
return NULL;
}
jlong values[] = {(jlong)stats.received, (jlong)stats.rejected, (jlong)stats.agents,
(jlong)stats.mappedTuples, (jlong)stats.pendingRequests, (jlong)stats.notifications, (jlong)stats.duplicates};
jlongArray result = (*env)->NewLongArray(env, 7);
if (result) (*env)->SetLongArrayRegion(env, result, 0, 7, values);
return result;
}
Loading