Linux C++ ABI Compatibility When Deploying .so Files to Older Systems

Overview

When deploying shared object (.so) files to Linux-based hardware systems, binaries built on newer Linux distributions may fail at runtime on older systems. This can occur even when CPU architecture and operating system family are the same.

Failures may appear as loader errors, unresolved symbols, or crashes during library initialization.


C++ ABI Compatibility on Linux

On Linux, C++ binaries dynamically link against libstdc++, which provides a defined set of C++ ABI symbols. Newer Linux distributions ship versions of libstdc++ that expose additional ABI symbols (for example, CXXABI_1.3.8).

Older Linux systems may only provide earlier ABI versions, such as CXXABI_1.3.7. If a shared library is built on a system whose libstdc++ exports newer ABI symbols than are available on the target system, the binary may fail to load or execute at runtime.

This mismatch may not be detected during compilation.


Inspecting ABI Versions

The ABI symbols provided by libstdc++ on a given system can be inspected using:

strings /usr/lib/x86_64-linux-gnu/libstdc++.so.6 | grep CXXABI_1.3

This output can be used to compare ABI support between build and runtime environments.


Build Environment

Shared libraries were rebuilt using an older Linux distribution whose default libstdc++ did not expose newer ABI symbols.

The build environment was created using Docker, and Linux binaries were built inside that environment


SSH Configuration in the Build Environment

When using the older Linux distribution in the build environment, additional steps were required to enable SSH access.

A password was first set for the root account, and SSH was configured to allow password-based login. The following changes were made inside the container:

docker exec -it <container-name> bash
 passwd
sed -i 's/PasswordAuthentication no/PasswordAuthentication yes/' /etc/ssh/sshd_config
sed -i 's/#PasswordAuthentication yes/PasswordAuthentication yes/' /etc/ssh/sshd_config
sed -i 's/PermitRootLogin without-password/PermitRootLogin yes/' /etc/ssh/sshd_config
sed -i 's/PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config

echo "PasswordAuthentication yes" >> /etc/ssh/sshd_config
echo "PermitRootLogin yes" >> /etc/ssh/sshd_config

mkdir -p /var/run/sshd
service ssh restart

After these steps, SSH access to the container was available.


Building the Shared Libraries

In this container, the .so files were rebuilt and no longer depended on newer ABI symbols.


Summary

  • Linux shared library compatibility can be affected by C++ ABI differences.
  • ABI requirements are determined by the build environment’s libstdc++.
  • Newer Linux systems may introduce ABI symbols not present on older systems.
  • Rebuilding shared libraries in an environment aligned with the target system can resolve runtime compatibility issues.

This case reflects JNBridge’s ongoing work supporting customers with specialized deployment requirements.

Changing to TCP: Configuration, SSL, Whitelisting & Startup

This post continues from our earlier walkthrough, Anatomy of a .NET Calling Java Project (Shared Memory), and uses the same BridgeDemo project to demonstrate how to change from Shared Memory to TCP communication.

While Shared Memory offers high performance for co-located components, TCP mode is used when the .NET and Java sides run on different machines. When using TCP, you can optionally enable additional features such as SSL encryption, IP whitelisting, and class whitelisting — features that aren’t applicable to Shared Memory connections.

1) Overview of the TCP Bridge

When the bridge runs in TCP mode, the .NET side and the Java side communicate through a socket connection rather than shared memory.
This offers several advantages:

  - Cross-platform independence – the .NET and Java components can run on different hosts or operating systems.
  - Remote accessibility – the bridge can connect across LAN or WAN networks.
  - Security options – TCP supports SSL, IP whitelisting, and class-level restrictions(class whitelisting).

Unlike Shared Memory, the Java side must be started first in TCP mode so that the .NET side can connect to it.

2) Updating the .NET Configuration (App.config)

In BridgeDemo, open App.config and locate the <dotNetToJavaConfig> section.
Change the scheme from sharedmem to jtcp, and specify the host, port, and optional SSL parameters.

  - scheme – must be set to jtcp
  - host – the machine where the Java side is running
  - port – must match the port defined in java.properties

Example:

<jnbridge>
    <dotNetToJavaConfig
      scheme="jtcp"
      host="localhost"
      port="8085"
      useSSL="true"
      clientCertificateLocation=".\testclient.p12"
      clientCertificatePassword="pw"
      sslAlternateServerNames="myServer" />
    />
</jnbridge>

If you are not using SSL, simply omit those attributes or set useSSL to false.

3) Updating the Java-Side Configuration (java.properties)

On the Java side, update java.properties to switch the bridge to TCP, define the connection port, and enable optional security features like IP and class whitelisting or SSL.

Example — javaSide.properties

javaSide.serverType=tcp
javaSide.workers=5
javaSide.timeout=10000
javaSide.port=8085
javaSide.useClassWhiteList=true
javaSide.classWhiteListFile=./classWhiteList.txt
dotNetSide.ipWhitelist=127.0.0.1
javaSide.keyStore=./keystore.jks
javaSide.keyStorePassword=changeit
javaSide.trustStore=./cacerts.jks
javaSide.trustStorePassword=changeit

4) Adding Whitelisting (Class and IP)

When using TCP, you can restrict which classes are exposed to .NET and which clients are allowed to connect. These options improve security and control without affecting performance.

a) Class Whitelisting

Create a file named classWhiteList.txt in the same directory as java.properties.
Each line specifies a fully qualified Java class name that can be accessed from the .NET side.

Example — classWhiteList.txt

             bridgeDemo.JavaToCall

Then, enable class whitelisting in java.properties:

       javaSide.useClassWhiteList=true
javaSide.classWhiteListFile=./classWhiteList.txt

Only the classes listed here can be loaded by the bridge; all others are blocked automatically.

b) IP Whitelisting

IP whitelisting limits which machines can connect to the Java bridge.
Add the following property to java.properties:

        dotNetSide.ipWhitelist=127.0.0.1

5) Enabling SSL

 SSL enables encryption, server authentication, and client authentication, protecting both sides of the bridge and preventing unauthorized access or code execution.

To implement SSL, you must complete all of the steps outlined in the “Secure communications using SSL” section of the User Guide(p.78)

1) Generate and install certificates
-Each Java side requires an X.509 server certificate whose Common Name (CN) matches the host where the Java side runs.
-Each .NET side requires its own client certificate, consisting of a public certificate and a corresponding private-key file.

2) Create and install the keystore and truststore on the Java side
-Install the Java side’s server certificate in a keystore file.
-Install each authorized .NET side’s client certificate in a truststore file.
-Both files must have passwords and be referenced in the JavaSide.properties file:

3) Create and install the keystore and truststore on the Java side
-Import the client certificate into the Windows Certificate Store.
-Leave the public certificate file accessible on disk so it can be referenced in the bridge configuration.

4) Configure the .NET side for SSL
-specify properties in app.config for SSL

After these steps are completed, all communication between .NET and Java occurs over an encrypted, mutually authenticated SSL channel.
For example, when .NET calls Ping.incr(1) the invocation is transmitted through this secure connection. The Java server’s identity is verified, the .NET client is authenticated, and all data is protected from interception.
The IP and class whitelists, worker-thread pool, and timeout settings continue to apply normally within this secure channel.

5) Starting the Java side

       – The Java side must be started manually before any .NET connections are made.

       – This launches the JNBridge Java component, loads all required classes, and applies the configuration defined in the properties file.

       – Use the following command:

java -cp “.;javaToCall.jar;jnbcore.jar” com.jnbridge.jnbcore.JNBMain /props “javaSide.properties”

This command must be run from the directory that contains the listed JAR files and the javaSide.properties file. When you execute it, Java uses the classpath (-cp) to locate javaToCall.jar (your Java classes) and jnbcore.jar (the JNBridge runtime).

 

Anatomy of a .NET Project That Calls Java (Shared Memory Bridge)

1) What We’re Building

In this post we’ll break down the anatomy of a simple interop project where a .NET application calls into a Java class using JNBridgePro with shared memory.

The goal is to make the moving parts visible:

       – how a proxy connects the .NET and Java worlds,

       – what files live on each side,

      – other considerations for shared memory setups.

Switching this project to TCP (plus added considerations like SSL, IP/class whitelisting, and Java-side startup) will be covered in a separated post: Changing to TCP: Configuration, SSL, Whitelisting & Startup.

2) Project Layout

Here’s the directory structure of a demo project (BridgeDemo). At a glance, you can see two main areas:

  - .NET project (BridgeDemo) – the C# application, configuration file, and runtime DLLs.

  - Java side (Java Side) – the original Java class and supporting JARs and properties.

BridgeDemo/
├── BridgeDemo.sln
├── BridgeDemo/    # .NET project
│   ├── Program.cs
│   ├── App.config
│   ├── bridgeDemo.dll    # Added as reference to project, built by JNBProxyGenerator from JavaClass or JAR
│   ├── JNBShare.dll   # Added as reference to project
│   ├── JNBSharedMem_x86/x64.dll
│   ├── jnbauth_x86/x64.dll
│   └── bin/Release/...    # Executable + runtime dlls
├── Java Side/    # Java side
│   ├── bridgeDemo/javaToCall.java,javaToCall.class
│   ├── jnbcore.jar
│   ├── javaToCall.jar          # Java dependency in this demo
│   ├── bcel-6.10.0.jar    # Only needed for shared mem
│   ├── javaSide.properties
│   ├── classWhiteList.txt
│   └── start Java side.bat

3) .NET Side

On the .NET side, the project is built around referenced assemblies, supporting DLLs, and the App.config that defines how it connects to the Java side.

Referenced assemblies

       – bridgeDemo.dll – the proxy assembly that exposes the Java class to .NET.

       – JNBShare.dll – the core bridge library required for all JNBridge communication.

Both are added under References in Visual Studio and have Copy Local = True, ensuring they’re automatically placed in the output folder when you build. This way, BridgeDemo.exe can find them at runtime without extra configuration.

Supporting DLLs

  - JNBSharedMem_x86.dll / JNBSharedMem_x64.dll – required only when using the Shared Memory transport.

  - jnbauth_x86.dll / jnbauth_x64.dll – used internally by the bridge for authentication and setup.

These supporting DLLs are not referenced in the project. They simply need to be present in the same directory as BridgeDemo.exe.

App.config
At runtime, BridgeDemo.exe uses this configuration to tell JNBridge how to locate and load the Java side.
Below is a sample configuration for the Shared Memory transport.

<jnbridge>
    <dotNetToJavaConfig
      scheme="sharedmem"
      jvm64="C:\Program Files\Java\jdk-25\bin\server\jvm.dll"
      jnbcore="C:\Projects\BridgeDemo\Java Side\jnbcore.jar"
      bcel="C:\Projects\BridgeDemo\Java Side\SharedMemory\bcel-6.10.0.jar"
      classpath="C:\Projects\BridgeDemo\Java Side;C:\Projects\BridgeDemo\Java Side\javaToCall.jar;"
    />
</jnbridge>

  - scheme – transport type (sharedmem or tcp).

  - jvm64 – points to the 64-bit JVM to load (jvm32 used if needed).

  - jnbcore – path to jnbcore.jar, which contains the bridge runtime.

  - bcel – only required when using Shared Memory; omitted for TCP.

  - classpath – tells JNBridge where to find your Java classes or JARs (the Java Side folder in this project).

The classpath section determines whether the .NET side connects to a Java package directory containing .class files, a JAR, or both if listed together.

4) Java Side

The Java side hosts the actual code and bridge runtime that the .NET process connects to. It generally includes core/supporting JARs, the Java code or JAR, and a properties file defining how the JVM bridge runs.

Core and supporting JARs

  - jnbcore.jar – always required; contains the bridge runtime and handles all communication.

  - bcel-6.10.0.jar – only needed for Shared Memory transport.

Java code or packaged JAR
You may have either of the following, depending on how your build is structured:

  - A package directory (for example, BridgeDemo/) with .class files.

  - Or a JAR archive (such as BridgeDemo.jar) containing the compiled classes.

Which of these is used depends on the classpath specified in the .NET project’s App.config.

Java-side properties file
This file (for example, javaSide.properties) controls how the JVM runs the bridge. Typical entries include:

javaSide.serverType=sharedmem
javaSide.timeout=10000
javaSide.port=8085 
javaSide.useClassWhiteList=false 
javaSide.classWhiteListFile=./classWhiteList.txt
javaSide.useSSL=false

5) Considerations for Shared Memory setups

Shared Memory provides the fastest .NET↔Java communication since both run in the same process, but it requires correct architecture alignment and file placement.

The JVM architecture must match your .NET build target.

  - 64-bit .NET → use 64-bit jvm.dll.

  - 32-bit .NET → use 32-bit jvm.dll.

Supporting DLLs must also match:

  - JNBSharedMem_x86.dll / JNBSharedMem_x64.dll

  - jnbauth_x86.dll / jnbauth_x64.dll

You can omit the unused architecture’s DLLs (for example, keep only the x64 set for a 64-bit-only build).

If the app may run in both modes (for example, AnyCPU), include all four — JNBridge will automatically load the correct pair at runtime.

All bridge DLLs (JNBShare.dll, JNBSharedMem_x*.dll, jnbauth_x*.dll) must reside in the same directory as your executable.

Handling ClassNotFoundException and NoClassDefFoundError

These errors during shared-memory startup usually come from missing permissions or classloader issues in older JVMs.

  - Permissions – Ensure the user running the app has Read & Execute, List Folder Contents, and Read access to all Java class and JAR folders (jnbcore.jar, bcel-6.x.x.jar, and any app JARs). Missing access prevents the JVM from loading classes, especially under ASP.NET.

  - Classloader conflicts (Java 8 and earlier) – If permissions are correct but errors persist, move the affected .jar from the classpath to the boot classpath using -Xbootclasspath/p: and remove it from the regular classpath.

<jnbridge>
    <dotNetToJavaConfig
      scheme="sharedmem"
      jvm64="C:\Program Files\Java\jdk-25\bin\server\jvm.dll"
      jnbcore="C:\Projects\BridgeDemo\Java Side\jnbcore.jar"
      bcel="C:\Projects\BridgeDemo\Java Side\SharedMemory\bcel-6.10.0.jar"
      classpath="C:\Projects\BridgeDemo\Java Side;"
      jvmOptions.0="-Xbootclasspath/p:C:\Projects\BridgeDemo\Java Side\javaToCall.jar"
    />
</jnbridge>

– In this example, javaToCall.jar was removed from the classpath and added to the boot classpath, allowing the JVM to load it earlier and resolve the conflict.
  (The -Xbootclasspath/p: option works only through Java 8 and was removed in Java 9+.)