Skip to content

Commit 8775247

Browse files
authored
Merge pull request #5 from Ceilidh-Team/factory
Added new factory pattern
2 parents cd7737c + 78d26c5 commit 8775247

File tree

12 files changed

+680
-633
lines changed

12 files changed

+680
-633
lines changed

NativeTK.sln

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ProjectCeilidh.NativeTK.Tes
99
EndProject
1010
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ProjectCeilidh.NativeTK.Benchmark", "ProjectCeilidh.NativeTK.Benchmark\ProjectCeilidh.NativeTK.Benchmark.csproj", "{C0F89064-2E15-4EB7-9311-8F3ED02DF5E8}"
1111
EndProject
12-
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProjectCeilidh.NativeTK.Example", "ProjectCeilidh.NativeTK.Example\ProjectCeilidh.NativeTK.Example.csproj", "{9D29B4A4-6370-420A-B1E5-CC980A944619}"
12+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ProjectCeilidh.NativeTK.Example", "ProjectCeilidh.NativeTK.Example\ProjectCeilidh.NativeTK.Example.csproj", "{9D29B4A4-6370-420A-B1E5-CC980A944619}"
1313
EndProject
1414
Global
1515
GlobalSection(SolutionConfigurationPlatforms) = preSolution

ProjectCeilidh.NativeTK.Benchmark/Program.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ public class Program
1212
[RankColumn, MedianColumn]
1313
public class Benchmark
1414
{
15-
private static readonly ITestBinding IndirectBinding = BindingFactory.CreateBinding<ITestBinding>(NativeBindingType.Indirect);
16-
private static readonly ITestBinding StaticBinding = BindingFactory.CreateBinding<ITestBinding>();
15+
private static readonly ITestBinding IndirectBinding = BindingFactory.GetFactoryForBindingType(NativeBindingType.Indirect).CreateBinding<ITestBinding>();
16+
private static readonly ITestBinding StaticBinding = BindingFactory.GetFactoryForBindingType(NativeBindingType.Static).CreateBinding<ITestBinding>();
1717

1818
[Benchmark]
1919
public void Indirect()

ProjectCeilidh.NativeTK.Example/Program.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ public class Program
77
{
88
public static void Main(string[] args)
99
{
10-
var ebur128 = BindingFactory.CreateBinding<IEbuR128>();
10+
var ebur128 = BindingFactory.GetFactoryForBindingType(NativeBindingType.Static).CreateBinding<IEbuR128>();
1111
ebur128.GetVersion(out var major, out var minor, out var patch);
1212
Console.WriteLine($"Version {major}.{minor}.{patch}");
1313

ProjectCeilidh.NativeTK.Tests/BindingTests.cs

+5-3
Original file line numberDiff line numberDiff line change
@@ -12,21 +12,23 @@ public class BindingTests
1212
[InlineData(NativeBindingType.Static)]
1313
public void BindingTest(NativeBindingType bindingType)
1414
{
15+
var factory = BindingFactory.GetFactoryForBindingType(bindingType);
16+
1517
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
1618
{
17-
var binding = BindingFactory.CreateBinding<ITestBindingWindows>(bindingType);
19+
var binding = factory.CreateBinding<ITestBindingWindows>();
1820
Assert.NotEqual(IntPtr.Zero, binding.GetStdHandle(-11));
1921
ref var _ = ref binding.GetCommandLineWRef;
2022
}
2123
else
2224
{
23-
var binding = BindingFactory.CreateBinding<ITestBindingUnix>(bindingType);
25+
var binding = factory.CreateBinding<ITestBindingUnix>();
2426
Assert.Equal(IntPtr.Zero, binding.dlopen("", 0));
2527
ref var _ = ref binding.dlsym;
2628
}
2729
}
2830

29-
[NativeLibraryContract("dl", VersionString = "2.0.0")]
31+
[NativeLibraryContract("dl")]
3032
public interface ITestBindingUnix
3133
{
3234
[NativeImport]

ProjectCeilidh.NativeTK/Attributes/NativeLibraryContractAttribute.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ public class NativeLibraryContractAttribute : Attribute
1717
/// <summary>
1818
/// The version of the library, as a string.
1919
/// </summary>
20-
public string VersionString = "0.0.0";
20+
public string VersionString = null;
2121
/// <summary>
2222
/// A version object reperesenting the version string.
2323
/// </summary>

ProjectCeilidh.NativeTK/BindingFactory.cs

+22-620
Large diffs are not rendered by default.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
using System;
2+
using System.IO;
3+
using System.Reflection;
4+
using System.Runtime.InteropServices;
5+
using Mono.Cecil;
6+
using Mono.Cecil.Cil;
7+
using ProjectCeilidh.NativeTK.Attributes;
8+
using ProjectCeilidh.NativeTK.Native;
9+
10+
namespace ProjectCeilidh.NativeTK
11+
{
12+
public class IndirectBindingFactory : BindingFactory
13+
{
14+
public override NativeBindingType BindingType => NativeBindingType.Indirect;
15+
16+
public override object CreateBinding(Type contractType)
17+
{
18+
if (RuntimeInformation.ProcessArchitecture == Architecture.X86) throw new PlatformNotSupportedException();
19+
20+
// Get the NativeLibraryLoader for this platform
21+
var loader = NativeLibraryLoader.GetLibraryLoaderForPlatform();
22+
23+
if (!contractType.IsInterface) throw new ArgumentException("Type argument must be an interface.");
24+
25+
var libraryAttr = contractType.GetCustomAttribute<NativeLibraryContractAttribute>();
26+
27+
if (libraryAttr == null) throw new ArgumentException("Type argument must have a NativeLibraryContractAttribute");
28+
29+
var handle = loader.LoadNativeLibrary(libraryAttr.LibraryName, libraryAttr.Version);
30+
31+
// Create the dynamic assembly which will contain the binding
32+
var asm = AssemblyDefinition.CreateAssembly(new AssemblyNameDefinition(libraryAttr.LibraryName, libraryAttr.Version),
33+
"<Module>", ModuleKind.Dll);
34+
// Create the binding type
35+
var implTyp = new TypeDefinition("", $"{contractType.Name}Impl", Mono.Cecil.TypeAttributes.Public, asm.MainModule.TypeSystem.Object);
36+
implTyp.Interfaces.Add(new InterfaceImplementation(asm.MainModule.ImportReference(contractType)));
37+
asm.MainModule.Types.Add(implTyp);
38+
39+
// Create a default constructor for the binding type
40+
var implCtor = new MethodDefinition(".ctor",
41+
Mono.Cecil.MethodAttributes.SpecialName | Mono.Cecil.MethodAttributes.RTSpecialName | Mono.Cecil.MethodAttributes.Public |
42+
Mono.Cecil.MethodAttributes.HideBySig, asm.MainModule.TypeSystem.Void);
43+
implTyp.Methods.Add(implCtor);
44+
// Simple ctor body - load `this`, call `new object()` against it
45+
var ctorProc = implCtor.Body.GetILProcessor();
46+
ctorProc.Emit(OpCodes.Ldarg_0);
47+
ctorProc.Emit(OpCodes.Call, asm.MainModule.ImportReference(typeof(object).GetConstructor(new Type[0])));
48+
ctorProc.Emit(OpCodes.Ret);
49+
50+
// Implement all the methods in the interface
51+
foreach (var intMethod in contractType.GetMethods())
52+
{
53+
// If the method has a special name, ignore it. This excludes property getters/setters
54+
if (intMethod.IsSpecialName) continue;
55+
56+
// The method cannot have varargs (this actually /can/ be achieved later, but it's too complicated for now)
57+
if (intMethod.CallingConvention == CallingConventions.VarArgs) throw new ArgumentException("Type argument cannot contain a method with varargs");
58+
59+
var intAttr = intMethod.GetCustomAttribute<NativeImportAttribute>();
60+
61+
if (intAttr == null) throw new ArgumentException($"Type argument contains a method without a NativeImportAttribute ({intMethod.Name})");
62+
63+
// Create the dynamic method for the implementation
64+
var meth = new MethodDefinition(intMethod.Name,
65+
Mono.Cecil.MethodAttributes.Public | Mono.Cecil.MethodAttributes.Final |
66+
Mono.Cecil.MethodAttributes.Virtual,
67+
asm.MainModule.ImportReference(intMethod.ReturnType));
68+
implTyp.Methods.Add(meth);
69+
70+
// The body for the dynamic method
71+
var proc = meth.Body.GetILProcessor();
72+
73+
// Generate a CallSite for the unmanaged function
74+
var callSite = new CallSite(asm.MainModule.ImportReference(intMethod.ReturnType));
75+
76+
switch (intAttr.CallingConvention)
77+
{
78+
case CallingConvention.Cdecl:
79+
callSite.CallingConvention = MethodCallingConvention.C;
80+
break;
81+
case CallingConvention.FastCall:
82+
callSite.CallingConvention = MethodCallingConvention.FastCall;
83+
break;
84+
case CallingConvention.Winapi:
85+
case CallingConvention.StdCall:
86+
callSite.CallingConvention = MethodCallingConvention.StdCall;
87+
break;
88+
case CallingConvention.ThisCall:
89+
callSite.CallingConvention = MethodCallingConvention.ThisCall;
90+
break;
91+
default:
92+
throw new ArgumentOutOfRangeException();
93+
}
94+
95+
var i = 0;
96+
foreach (var param in intMethod.GetParameters())
97+
{
98+
callSite.Parameters.Add(
99+
new ParameterDefinition(param.Name, (Mono.Cecil.ParameterAttributes)param.Attributes,
100+
asm.MainModule.ImportReference(param.ParameterType)));
101+
meth.Parameters.Add(new ParameterDefinition(param.Name, (Mono.Cecil.ParameterAttributes)param.Attributes,
102+
asm.MainModule.ImportReference(param.ParameterType)));
103+
104+
proc.Emit(OpCodes.Ldarg, ++i);
105+
}
106+
107+
// Load the symbol address for this function as a long, then convert to an IntPtr (native int)
108+
proc.Emit(OpCodes.Ldc_I8, (long)handle.GetSymbolAddress(intAttr.EntryPoint ?? intMethod.Name));
109+
proc.Emit(OpCodes.Conv_I);
110+
111+
// Invoke the method with a CallIndirect, then return the result
112+
proc.Emit(OpCodes.Calli, callSite);
113+
proc.Emit(OpCodes.Ret);
114+
}
115+
116+
// Implement all the properties in the interface
117+
foreach (var intProp in contractType.GetProperties())
118+
{
119+
var intAttr = intProp.GetCustomAttribute<NativeImportAttribute>();
120+
121+
if (intAttr == null) throw new ArgumentException($"Type argument contains a property without a NativeImportAttribute ({intProp.Name})");
122+
123+
if (!intProp.PropertyType.IsByRef) throw new ArgumentException($"Type argument's properties must be ref returns ({intProp.Name})");
124+
if (intProp.CanWrite) throw new ArgumentException($"Type argument's properties cannot have a setter ({intProp.Name})");
125+
126+
if (intProp.PropertyType.GetElementType()?.IsValueType != true) throw new ArgumentException("Type argument's properties must be a reference to a value type");
127+
128+
// Generate the property and get method
129+
var prop = new PropertyDefinition(intProp.Name, Mono.Cecil.PropertyAttributes.None,
130+
asm.MainModule.ImportReference(intProp.PropertyType));
131+
var propMethod = new MethodDefinition($"get_{intProp.Name}",
132+
Mono.Cecil.MethodAttributes.Public | Mono.Cecil.MethodAttributes.Virtual | Mono.Cecil.MethodAttributes.Final |
133+
Mono.Cecil.MethodAttributes.SpecialName, asm.MainModule.ImportReference(intProp.PropertyType));
134+
prop.GetMethod = propMethod;
135+
implTyp.Properties.Add(prop);
136+
implTyp.Methods.Add(propMethod);
137+
138+
// Generate a get body which resolves the symbol
139+
var getProc = propMethod.Body.GetILProcessor();
140+
141+
// Load the symbol address and convert to an IntPtr (native int)
142+
getProc.Emit(OpCodes.Ldc_I8, (long)handle.GetSymbolAddress(intAttr.EntryPoint ?? intProp.Name));
143+
getProc.Emit(OpCodes.Conv_I);
144+
// Return this unmodified - the result is that the pointer is converted to a reference by the CLR
145+
getProc.Emit(OpCodes.Ret);
146+
}
147+
148+
// Write the newly generated assembly to memory, load it, and instatiate the newly generated binding
149+
using (var mem = new MemoryStream())
150+
{
151+
asm.Write(mem);
152+
var newAsm = Assembly.Load(mem.ToArray());
153+
var newTyp = newAsm.GetType($"{contractType.Name}Impl");
154+
return newTyp.GetConstructor(new Type[0])?.Invoke(new object[0]);
155+
}
156+
}
157+
}
158+
}

ProjectCeilidh.NativeTK/Native/Platform/LinuxNativeLibraryLoader.cs

+8-3
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,19 @@ internal class LinuxNativeLibraryLoader : NativeLibraryLoader
1010

1111
protected override string[] GetNativeLibraryNames(string libraryName, Version version)
1212
{
13+
if (version == null)
14+
return new[]
15+
{
16+
$"lib{libraryName}.so"
17+
};
18+
1319
return new[]
1420
{
1521
$"lib{libraryName}.so.{version.Major}.{version.Minor}.{version.Build}",
1622
$"lib{libraryName}.so.{version.Major}.{version.Minor}",
17-
$"lib{libraryName}.so.{version.Major}",
18-
$"lib{libraryName}.so"
23+
$"lib{libraryName}.so.{version.Major}"
1924
};
20-
}
25+
}
2126

2227
protected override NativeLibraryHandle LoadNativeLibrary(string libraryName)
2328
{

ProjectCeilidh.NativeTK/Native/Platform/MacNativeLibraryLoader.cs

+7-2
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,17 @@ internal class MacNativeLibraryLoader : NativeLibraryLoader
1010

1111
protected override string[] GetNativeLibraryNames(string libraryName, Version version)
1212
{
13+
if (version == null)
14+
return new[]
15+
{
16+
$"lib{libraryName}.dylib"
17+
};
18+
1319
return new[]
1420
{
1521
$"lib{libraryName}.{version.Major}.{version.Minor}.{version.Build}.dylib",
1622
$"lib{libraryName}.{version.Major}.{version.Minor}.dylib",
17-
$"lib{libraryName}.{version.Major}.dylib",
18-
$"lib{libraryName}.dylib"
23+
$"lib{libraryName}.{version.Major}.dylib"
1924
};
2025
}
2126

ProjectCeilidh.NativeTK/Native/Platform/WindowsNativeLibraryLoader.cs

+9
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,15 @@ protected override string[] GetNativeLibraryNames(string libraryName, Version ve
1111
{
1212
var archDescription = IntPtr.Size == 4 ? "x86" : "x64";
1313

14+
if (version == null)
15+
return new[]
16+
{
17+
$"{libraryName}.dll",
18+
$"lib{libraryName}.dll",
19+
$"{libraryName}_{archDescription}.dll",
20+
$"lib{libraryName}_{archDescription}.dll"
21+
};
22+
1423
return new[]
1524
{
1625
$"{libraryName}-{version.Major}.dll",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
namespace ProjectCeilidh.NativeTK
2+
{
3+
/// <summary>
4+
/// The strategy used to create the native binding
5+
/// </summary>
6+
public enum NativeBindingType
7+
{
8+
/// <summary>
9+
/// Bind native functions using the CallIndirect opcode.
10+
/// This has poor compatibility and does not support marshaling options
11+
/// </summary>
12+
Indirect = 1,
13+
/// <summary>
14+
/// Bind native functions using PInvokeImpl.
15+
/// This has the best compatibility, supporting marshaling options, but is slightly slower.
16+
/// </summary>
17+
Static = 2
18+
}
19+
}

0 commit comments

Comments
 (0)