Skip to content
Merged
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
1 change: 1 addition & 0 deletions .yamato/standalone-build-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,5 @@ test_linux_standalone_{{ editor.version }}_{{ editor.extra_test }}:
paths:
- "artifacts/testPlayer*/**"
- "artifacts/**/UnityPlayer.so"
- "artifacts/CoreCLR/**"
{% endfor %}
1 change: 1 addition & 0 deletions com.unity.ml-agents/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ and this project adheres to
- Make `BehaviorParametersEditor` create new sensors only when needed. (#6318, #6323)
- Fixed a `NullReferenceException` thrown from the `TensorProxy` finalizer, which is reported as an unhandled exception on CoreCLR. (#6335)
- Fix sensor management of CameraSensorComponent and RenderTextureSensorComponent (#6320, #6324)
- Fixed Linux players and editors on CoreCLR failing to connect to the trainer because gRPC could not load `libdl.so`.

## [4.1.0] - 2026-07-27
### Major Changes
Expand Down
99 changes: 99 additions & 0 deletions com.unity.ml-agents/Runtime/Communicator/GrpcNativeLoader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
#if UNITY_EDITOR || UNITY_STANDALONE
using System;
using System.Reflection;
using System.Runtime.InteropServices;
using UnityEngine;

namespace Unity.MLAgents
{
/// <summary>
/// Works around Grpc.Core 1.10 failing to load its native extension on Linux under CoreCLR.
///
/// Grpc.Core only special-cases Mono and ".NET Core"; on any other runtime (e.g. Unity's CoreCLR)
/// it P/Invokes dlopen/dlsym from "libdl.so". That unversioned name only exists when glibc dev
/// packages are installed, so loading fails on most machines (only "libdl.so.2" is present).
/// We hook the load context's ResolvingUnmanagedDll event, which is only raised after the default
/// lookup fails, and redirect "libdl.so" to "libdl.so.2" (or libc, which exports dlopen on glibc 2.34+).
///
/// Everything is done via reflection so this compiles against all API profiles and is a no-op on
/// Mono and IL2CPP, where these types don't exist.
/// </summary>
internal static class GrpcNativeLoader
{
static readonly string[] k_LibdlCandidates = { "libdl.so.2", "libc.so.6" };

static bool s_Installed;
static MethodInfo s_TryLoad;

internal static void EnsureInstalled()
{
if (s_Installed)
{
return;
}
s_Installed = true;

if (Application.platform != RuntimePlatform.LinuxPlayer &&
Application.platform != RuntimePlatform.LinuxEditor &&
Application.platform != RuntimePlatform.LinuxServer)
{
return;
}

try
{
var coreLib = typeof(Marshal).Assembly;
var nativeLibrary = coreLib.GetType("System.Runtime.InteropServices.NativeLibrary");
var loadContextType = coreLib.GetType("System.Runtime.Loader.AssemblyLoadContext");
if (nativeLibrary == null || loadContextType == null)
{
// Not running on CoreCLR (Mono / IL2CPP): Grpc.Core handles these itself.
return;
}

s_TryLoad = nativeLibrary.GetMethod(
"TryLoad", new[] { typeof(string), typeof(IntPtr).MakeByRefType() });
var getLoadContext = loadContextType.GetMethod(
"GetLoadContext", BindingFlags.Public | BindingFlags.Static, null, new[] { typeof(Assembly) }, null);
var resolvingEvent = loadContextType.GetEvent("ResolvingUnmanagedDll");
if (s_TryLoad == null || getLoadContext == null || resolvingEvent == null)
{
return;
}

var grpcAssembly = typeof(Grpc.Core.Channel).Assembly;
var loadContext = getLoadContext.Invoke(null, new object[] { grpcAssembly });
if (loadContext == null)
{
return;
}

Func<Assembly, string, IntPtr> handler = Resolve;
resolvingEvent.AddEventHandler(loadContext, handler);
}
catch (Exception e)
{
Debug.LogWarning($"ML-Agents: unable to install gRPC native library fallback: {e}");
}
}

static IntPtr Resolve(Assembly assembly, string libraryName)
{
if (libraryName != "libdl.so" || s_TryLoad == null)
{
return IntPtr.Zero;
}

foreach (var candidate in k_LibdlCandidates)
{
var args = new object[] { candidate, IntPtr.Zero };
if ((bool)s_TryLoad.Invoke(null, args))
{
return (IntPtr)args[1];
}
}
return IntPtr.Zero;
}
}
}
#endif

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ void UpdateEnvironmentWithInput(UnityRLInputProto rlInput)
UnityInputProto Initialize(int port, UnityOutputProto unityOutput, out UnityInputProto unityInput)
{
m_IsOpen = true;
GrpcNativeLoader.EnsureInstalled();
m_Channel = new Channel($"localhost:{port}", ChannelCredentials.Insecure);

m_Client = new UnityToExternalProto.UnityToExternalProtoClient(m_Channel);
Expand Down
Loading