// // Copyright (c) 2010-2025 Antmicro // Copyright (c) 2011-2015 Realtime Embedded // // This file is licensed under the MIT License. // Full license text is available in 'licenses/MIT.txt'. // using System; using System.Linq; using Antmicro.Renode.Logging; using System.Runtime.InteropServices; using System.Reflection; using System.Reflection.Emit; using System.Collections.Generic; using Dynamitey; using Antmicro.Renode.Core; namespace Antmicro.Renode.Utilities.Binding { /// /// The NativeBinder class lets one bind managed delegates from given class to functions /// of a given native library and vice versa. /// public sealed class NativeBinder : IDisposable { static NativeBinder() { var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly( new AssemblyName(nameof(NativeBinder)), AssemblyBuilderAccess.Run); moduleBuilder = assemblyBuilder.DefineDynamicModule(nameof(NativeBinder)); } /// /// Initializes a new instance of the class /// and performs binding between the class and given library. /// /// /// Class to bind. /// /// /// Library file to bind. /// /// /// Please note that: /// /// /// This works now only with ELF libraries. /// /// /// You have to hold the reference to created native binder as long as the native functions /// can call managed one. /// /// /// You should dispose native binder after use to free memory taken by the native library. /// /// /// public NativeBinder(IEmulationElement classToBind, string libraryFile) { delegateStore = new object[0]; #if !PLATFORM_WINDOWS && !NET // According to https://github.com/dotnet/runtime/issues/26381#issuecomment-394765279, // mono does not enforce the restrictions on pinned GCHandle objects. // On .NET Core trying to pin unallowed object throws exception about non-primitive or non-blittable data. handles = new GCHandle[0]; #endif this.classToBind = classToBind; libraryAddress = SharedLibraries.LoadLibrary(libraryFile); libraryFileName = libraryFile; var importFields = classToBind.GetType().GetAllFields().Where(x => x.IsDefined(typeof(ImportAttribute), false)).ToList(); EnsureWrappersType(importFields); wrappersObj = CreateWrappersObject(); try { ResolveCallsToNative(importFields); ResolveCallsToManaged(); } catch { Dispose(); throw; } } public void Dispose() { DisposeInner(); GC.SuppressFinalize(this); } private void DisposeInner() { #if !PLATFORM_WINDOWS && !NET foreach(var handle in handles) { handle.Free(); } #endif if(libraryAddress != IntPtr.Zero) { SharedLibraries.UnloadLibrary(libraryAddress); libraryAddress = IntPtr.Zero; } } ~NativeBinder() { DisposeInner(); } private void EnsureWrappersType(List importFields) { // This type lives in our NativeBinder dynamic assembly (see the static constructor). // This means that if we find such a type, it has to have been created by us and we can // just use it without verifying its layout. // Its name is derived from the full name of the class to bind (including its namespace): for example, // the wrappers type for Some.Namespace.Class will be NativeBinder.Some.Namespace.ClassWrappers. var wrappersTypeName = $"NativeBinder.{classToBind.GetType().FullName}Wrappers"; lock(moduleBuilder) { wrappersType = moduleBuilder.GetType(wrappersTypeName); if(wrappersType == null) { var typeBuilder = moduleBuilder.DefineType(wrappersTypeName); typeBuilder.DefineField(nameof(ExceptionKeeper), typeof(ExceptionKeeper), FieldAttributes.Public); typeBuilder.DefineField(nameof(classToBind), classToBind.GetType(), FieldAttributes.Public); foreach(var field in importFields) { var attribute = (ImportAttribute)field.GetCustomAttributes(false).Single(x => x is ImportAttribute); if(attribute.UseExceptionWrapper) { typeBuilder.DefineField(field.Name, field.FieldType, FieldAttributes.Public); } } wrappersType = typeBuilder.CreateType(); } exceptionKeeperField = wrappersType.GetField(nameof(ExceptionKeeper)); instanceField = wrappersType.GetField(nameof(classToBind)); } } private object CreateWrappersObject() { var wrappers = Activator.CreateInstance(wrappersType); exceptionKeeperField.SetValue(wrappers, new ExceptionKeeper()); instanceField.SetValue(wrappers, classToBind); return wrappers; } private Delegate WrapImport(FieldInfo importField, FieldInfo innerField) { var throwExceptions = typeof(ExceptionKeeper).GetMethod(nameof(ExceptionKeeper.ThrowExceptions)); var importType = importField.FieldType; var invoke = importType.GetMethod("Invoke"); Type[] paramTypes = invoke.GetParameters().Select(p => p.ParameterType).ToArray(); Type[] paramTypesWithWrappersType = new Type[] { wrappersType }.Concat(paramTypes).ToArray(); // We need skipVisibility to handle methods that have a protected or private type as a parameter or return value. // Interesting tidbit: this access check is only performed by .NET Framework, Mono and .NET Core allow it // without skipVisibility. DynamicMethod method = new DynamicMethod(importField.Name, invoke.ReturnType, paramTypesWithWrappersType, wrappersType, skipVisibility: true); var il = method.GetILGenerator(); il.Emit(OpCodes.Ldarg_0); // wrappersType instance il.Emit(OpCodes.Ldfld, innerField); for(int i = 0; i < paramTypes.Length; ++i) { il.Emit(OpCodes.Ldarg, 1 + i); } il.EmitCall(OpCodes.Callvirt, invoke, null); // call inner il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldfld, exceptionKeeperField); il.EmitCall(OpCodes.Call, throwExceptions, null); // call ExceptionKeeper.ThrowExceptions il.Emit(OpCodes.Ret); return method.CreateDelegate(importType, wrappersObj); } private void ResolveCallsToNative(List importFields) { classToBind.NoisyLog("Binding managed -> native calls."); foreach (var field in importFields) { var attribute = (ImportAttribute)field.GetCustomAttributes(false).First(x => x is ImportAttribute); var cName = GetWrappedName(attribute.Name ?? GetCName(field.Name), attribute.UseExceptionWrapper); classToBind.NoisyLog(string.Format("(NativeBinder) Binding {1} as {0}.", field.Name, cName)); Delegate result = null; try { var address = SharedLibraries.GetSymbolAddress(libraryAddress, cName); // Dynamically create a non-generic delegate type and make one from a function pointer var invoke = field.FieldType.GetMethod("Invoke"); var delegateType = DelegateTypeFromParamsAndReturn(invoke.GetParameters().Select(p => p.ParameterType), invoke.ReturnType); var generatedDelegate = Marshal.GetDelegateForFunctionPointer(address, delegateType); // The method returned by GetDelegateForFunctionPointer is static on Mono, but not .NET var delegateTarget = generatedDelegate.Method.IsStatic ? null : generatedDelegate; // "Convert" the delegate from the dynamically-generated non-generic type // to the field type used in the bound class (which might be generic) result = Delegate.CreateDelegate(field.FieldType, delegateTarget, generatedDelegate.Method); } catch { if(!attribute.Optional) { throw; } } if(attribute.UseExceptionWrapper && result != null) { var innerField = wrappersType.GetField(field.Name); innerField.SetValue(wrappersObj, result); field.SetValue(classToBind, WrapImport(field, innerField)); } else { field.SetValue(classToBind, result); } } } private Delegate WrapExport(Type delegateType, MethodInfo innerMethod) { var addException = typeof(ExceptionKeeper).GetMethod(nameof(ExceptionKeeper.AddException)); var nativeUnwind = classToBind.GetType().GetMethod(nameof(INativeUnwindable.NativeUnwind)); Type[] paramTypes = innerMethod.GetParameters().Select(p => p.ParameterType).ToArray(); Type[] paramTypesWithWrappersType = new Type[] { wrappersType }.Concat(paramTypes).ToArray(); // We need skipVisibility to be able to access superclass private methods. var attacheeWrapper = new DynamicMethod(innerMethod.Name + "Wrapper", innerMethod.ReturnType, paramTypesWithWrappersType, wrappersType, skipVisibility: true); var il = attacheeWrapper.GetILGenerator(); LocalBuilder retval = null; if(innerMethod.ReturnType != typeof(void)) { retval = il.DeclareLocal(innerMethod.ReturnType); } var exception = il.DeclareLocal(typeof(Exception)); Label @try = il.BeginExceptionBlock(); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldfld, instanceField); for(int i = 0; i < paramTypes.Length; ++i) { il.Emit(OpCodes.Ldarg, 1 + i); } il.EmitCall(OpCodes.Call, innerMethod, null); if(retval != null) { il.Emit(OpCodes.Stloc, retval); } il.Emit(OpCodes.Leave, @try); il.BeginCatchBlock(typeof(Exception)); il.Emit(OpCodes.Stloc, exception); // We need this local because MSIL doesn't have an instruction il.Emit(OpCodes.Ldarg_0); // that swaps the top two values on the stack. il.Emit(OpCodes.Ldfld, exceptionKeeperField); il.Emit(OpCodes.Ldloc, exception); il.EmitCall(OpCodes.Call, addException, null); if(nativeUnwind != null) { il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldfld, instanceField); il.EmitCall(OpCodes.Call, nativeUnwind, null); } else { var typeName = classToBind.GetType().FullName; il.EmitWriteLine($"Export to type '{typeName}' which is not unwindable threw an exception."); // Print exceptions. var printExceptions = typeof(ExceptionKeeper).GetMethod(nameof(ExceptionKeeper.PrintExceptions)); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldfld, exceptionKeeperField); il.EmitCall(OpCodes.Call, printExceptions, null); // call ExceptionKeeper.PrintExceptions il.Emit(OpCodes.Ldc_I4_1); il.EmitCall(OpCodes.Call, typeof(Environment).GetMethod(nameof(Environment.Exit)), null); } il.EndExceptionBlock(); if(retval != null) { il.Emit(OpCodes.Ldloc, retval); } il.Emit(OpCodes.Ret); return attacheeWrapper.CreateDelegate(delegateType, wrappersObj); } private void ResolveCallsToManaged() { classToBind.NoisyLog("Binding native -> managed calls."); var symbols = SharedLibraries.GetAllSymbols(libraryFileName); var classMethods = classToBind.GetType().GetAllMethods().ToArray(); var exportedMethods = new List(); foreach(var originalCandidate in symbols.Where(x => x.Contains("renode_external_attach"))) { var candidate = FilterCppName(originalCandidate); var parts = candidate.Split(new [] { "__" }, StringSplitOptions.RemoveEmptyEntries); var cName = parts[2]; var expectedTypeName = parts[1]; var csName = cName.StartsWith('$') ? GetCSharpName(cName.Substring(1)) : cName; classToBind.NoisyLog("(NativeBinder) Binding {0} as {2} of type {1}.", cName, expectedTypeName, csName); // let's find the desired method var desiredMethodInfo = classMethods.FirstOrDefault(x => x.Name == csName); if(desiredMethodInfo == null) { throw new InvalidOperationException(string.Format("Could not find method {0} in a class {1}.", csName, classToBind.GetType().Name)); } if(!desiredMethodInfo.IsDefined(typeof(ExportAttribute), true)) { throw new InvalidOperationException( string.Format("Method {0} is exported as {1} but it is not marked with the Export attribute.", desiredMethodInfo.Name, cName)); } var parameterTypes = desiredMethodInfo.GetParameters().Select(p => p.ParameterType).ToList(); var actualTypeName = ShortTypeNameFromParamsAndReturn(parameterTypes, desiredMethodInfo.ReturnType); if(expectedTypeName != actualTypeName) { throw new InvalidOperationException($"Method {cName} has type {actualTypeName} but the native library expects {expectedTypeName}"); } exportedMethods.Add(desiredMethodInfo); // let's make the delegate instance var delegateType = DelegateTypeFromParamsAndReturn(parameterTypes, desiredMethodInfo.ReturnType); Delegate attachee; try { attachee = WrapExport(delegateType, desiredMethodInfo); } catch(ArgumentException e) { throw new InvalidOperationException($"Could not resolve call to managed: {e.Message}. Candidate is '{candidate}', desired method is '{desiredMethodInfo.ToString()}'"); } #if !PLATFORM_WINDOWS && !NET // according to https://blogs.msdn.microsoft.com/cbrumme/2003/05/06/asynchronous-operations-pinning/, // pinning is wrong (and it does not work on windows too)... // but both on linux & osx it seems to be essential to avoid delegates from being relocated handles = handles.Union(new [] { GCHandle.Alloc(attachee, GCHandleType.Pinned) }).ToArray(); #endif delegateStore = delegateStore.Union(new [] { attachee }).ToArray(); // let's make the attaching function delegate var attacherType = DelegateTypeFromParamsAndReturn(new [] { delegateType }, typeof(void), $"Attach{delegateType.Name}"); var address = SharedLibraries.GetSymbolAddress(libraryAddress, originalCandidate); var attacher = Marshal.GetDelegateForFunctionPointer(address, attacherType); // and invoke it attacher.FastDynamicInvoke(attachee); } // check that all exported methods were really exported and issue a warning if not var notExportedMethods = classMethods.Where(x => x.IsDefined(typeof(ExportAttribute), true)).Except(exportedMethods); foreach(var method in notExportedMethods) { classToBind.Log(LogLevel.Warning, "Method {0} is marked with Export attribute, but was not exported.", method.Name); } } private static string ShortTypeNameFromParamsAndReturn(IEnumerable parameterTypes, Type returnType) { var baseName = returnType == typeof(void) ? "Action" : "Func" + returnType.Name; var paramNames = parameterTypes.Select(p => p.Name); return string.Join("", paramNames.Prepend(baseName)).Replace("[]", "Array"); } private static string GetCName(string name) { var lastCapitalChar = 0; var cName = name.GroupBy(x => { if (char.IsUpper(x)) { lastCapitalChar++; } return lastCapitalChar; }).Select(x => x.Aggregate(string.Empty, (y, z) => y + char.ToLower(z))). Aggregate((x, y) => x + "_" + y); return cName; } private static string GetWrappedName(string name, bool useExceptionWrapper) { if(useExceptionWrapper) { return name + "_ex"; // Bind to exception wrapper instead of inner function } return name; } private static string GetCSharpName(string name) { var words = name.Split('_'); return words.Select(x => FirstLetterUpper(x)).Aggregate((x, y) => x + y); } private static string FirstLetterUpper(string str) { return str.Substring(0, 1).ToUpper() + str.Substring(1); } private static string FilterCppName(string name) { var result = Symbol.DemangleSymbol(name).Split('(')[0].ToString(); if(result.StartsWith("_")) { return result.Skip(4).ToString(); } return result; } private static Type GetUnderlyingType(Type type) { return type.IsEnum ? type.GetEnumUnderlyingType() : type; } // This method and the constants used in it are inspired by MakeNewCustomDelegate from .NET itself. // See https://github.com/dotnet/runtime/blob/8ca896c3f5ef8eb1317439178bf041b5f270f351/src/libraries/System.Linq.Expressions/src/System/Linq/Expressions/Compiler/DelegateHelpers.cs#L110 private static Type DelegateTypeFromParamsAndReturn(IEnumerable parameterTypes, Type returnType, string name = null) { // Convert enums to their underlying types to avoid creating needless additional delegate types returnType = GetUnderlyingType(returnType); parameterTypes = parameterTypes.Select(GetUnderlyingType); if(name == null) { // The default naming mirrors the generic delegate types, but for functions, the return type comes first // For example, Func becomes FuncUInt64Int32. name = ShortTypeNameFromParamsAndReturn(parameterTypes, returnType); } var delegateTypeName = $"NativeBinder.Delegates.{name}"; lock(moduleBuilder) { var delegateType = moduleBuilder.GetType(delegateTypeName); if(delegateType == null) { var delegateTypeAttributes = TypeAttributes.Class | TypeAttributes.Public | TypeAttributes.Sealed | TypeAttributes.AnsiClass | TypeAttributes.AutoClass; var delegateConstructorAttributes = MethodAttributes.RTSpecialName | MethodAttributes.HideBySig | MethodAttributes.Public; var delegateInvokeAttributes = MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.NewSlot | MethodAttributes.Virtual; var delegateMethodImplAttributes = MethodImplAttributes.Runtime | MethodImplAttributes.Managed; var typeBuilder = moduleBuilder.DefineType(delegateTypeName, delegateTypeAttributes, typeof(MulticastDelegate)); var constructor = typeBuilder.DefineConstructor(delegateConstructorAttributes, CallingConventions.Standard, new [] { typeof(object), typeof(IntPtr) }); constructor.SetImplementationFlags(delegateMethodImplAttributes); var invokeMethod = typeBuilder.DefineMethod("Invoke", delegateInvokeAttributes, returnType, parameterTypes.ToArray()); invokeMethod.SetImplementationFlags(delegateMethodImplAttributes); // Add the [UnmanagedFunctionPointer] attribute with Cdecl specified var unmanagedFnAttrCtor = typeof(UnmanagedFunctionPointerAttribute).GetConstructor(new [] { typeof(CallingConvention) }); var unmanagedFnAttrBuilder = new CustomAttributeBuilder(unmanagedFnAttrCtor, new object[] { CallingConvention.Cdecl }); typeBuilder.SetCustomAttribute(unmanagedFnAttrBuilder); delegateType = typeBuilder.CreateType(); } return delegateType; } } private static readonly ModuleBuilder moduleBuilder; private IntPtr libraryAddress; private string libraryFileName; private IEmulationElement classToBind; private Type wrappersType; private FieldInfo instanceField; private FieldInfo exceptionKeeperField; private object wrappersObj; // the point of delegate store is to hold references to delegates // which would otherwise be garbage collected while native calls // can still use them private object[] delegateStore; #if !PLATFORM_WINDOWS && !NET private GCHandle[] handles; #endif } }