├── .gitignore
├── ComLight.sln
├── ComLight
├── AssemblyInfo.cs
├── Cache
│ ├── Managed.cs
│ └── Native.cs
├── ComInterfaceAttribute.cs
├── ComLight.csproj
├── ComLightCast.cs
├── ComLightInterop.nuspec
├── ComLightInterop.targets
├── CustomConventionsAttribute.cs
├── DebuggerTypeProxyAttribute.cs
├── Emit
│ ├── Assembly.cs
│ ├── BaseInterfaces.cs
│ ├── DelegatesBuilder.cs
│ ├── NativeDelegates.cs
│ ├── PropertiesBuilder.cs
│ ├── Proxy.cs
│ ├── Proxy.custom.cs
│ └── Proxy.standard.cs
├── IO
│ ├── ManagedReadStream.cs
│ ├── ManagedWriteStream.cs
│ ├── NativeReadStream.cs
│ ├── NativeWriteStream.cs
│ ├── ReadStreamAttribute.cs
│ ├── ReadStreamMarshal.cs
│ ├── WriteStreamAttribute.cs
│ ├── WriteStreamMarshal.cs
│ ├── iReadStream.cs
│ └── iWriteStream.cs
├── IUnknown.cs
├── ManagedObject.cs
├── ManagedWrapper.cs
├── ManagedWrapper.impl.cs
├── Marshaler.cs
├── Marshalling
│ ├── Expressions.cs
│ ├── InterfaceArrayMarshaller.cs
│ ├── InterfaceMarshaller.cs
│ ├── MarshallerAttribute.cs
│ ├── Marshallers.cs
│ └── iCustomMarshal.cs
├── NativeStringAttribute.cs
├── NativeWrapper.cs
├── ParamsMarshalling.cs
├── PropertyAttribute.cs
├── Readme.md
├── RetValIndexAttribute.cs
├── RuntimeClass.cs
├── Utils
│ ├── EmitUtils.cs
│ ├── ErrorCodes.cs
│ ├── ManagedWrapperCache.cs
│ ├── MiscUtils.cs
│ ├── NativeWrapperCache.cs
│ ├── ReflectionUtils.cs
│ ├── errorCodez.cs
│ └── errorCodez.tt
└── iComDisposable.cs
├── ComLightDesktop
├── Cache
│ └── Native.cs
├── ComLightDesktop.csproj
├── IO
│ └── StreamExt.cs
└── app.config
├── ComLightLib
├── ComLightLib.vcxproj
├── ComLightLib.vcxproj.filters
├── Exception.hpp
├── client
│ └── CComPtr.hpp
├── comLightClient.h
├── comLightCommon.h
├── comLightServer.h
├── hresult.h
├── pal
│ ├── guiddef.h
│ └── hresult.h
├── server
│ ├── Object.hpp
│ ├── ObjectRoot.hpp
│ ├── RefCounter.hpp
│ ├── freeThreadedMarshaller.cpp
│ ├── freeThreadedMarshaller.h
│ └── interfaceMap.h
├── streams.h
├── unknwn.h
└── utils
│ ├── guid_parse.hpp
│ └── typeTraits.hpp
├── Demos
├── HelloWorldCS
│ ├── HelloWorld.cs
│ └── HelloWorldCS.csproj
├── HelloWorldCpp
│ ├── CMakeLists.txt
│ ├── HelloWorld.cpp
│ ├── HelloWorld.def
│ ├── HelloWorld.vcxproj
│ └── HelloWorld.vcxproj.filters
├── StreamsCS
│ ├── Streams.cs
│ └── StreamsCS.csproj
├── StreamsCpp
│ ├── CMakeLists.txt
│ ├── NativeFileSystem.cpp
│ ├── Streams.cpp
│ ├── Streams.def
│ ├── Streams.vcxproj
│ ├── Streams.vcxproj.filters
│ └── interfaces.h
└── StreamsDesktop
│ ├── App.config
│ ├── Properties
│ └── AssemblyInfo.cs
│ └── StreamsDesktop.csproj
├── DesktopClient
├── App.config
├── DesktopClient.csproj
├── Program.cs
└── Properties
│ └── AssemblyInfo.cs
├── DesktopTest
├── App.config
├── DesktopTest.csproj
├── Program.cs
└── Properties
│ └── AssemblyInfo.cs
├── LICENSE
├── NativeLibrary
├── CMakeLists.txt
├── ITest.h
├── NativeLibrary.vcxproj
├── NativeLibrary.vcxproj.filters
├── Test.cpp
├── Test.h
├── WriteStream.cpp
├── WriteStream.h
├── dllmain.cpp
├── library.def
├── stdafx.cpp
├── stdafx.h
└── targetver.h
├── PortableClient
├── ITest.cs
├── LinuxUtils.cs
├── ManagedImpl.cs
├── PortableClient.csproj
├── Program.cs
├── Properties
│ └── launchSettings.json
├── TestClass.cs
└── Tests.cs
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | .vs/
2 | ComLight/obj/
3 | ComLightDesktop/bin/
4 | ComLightDesktop/obj/
5 | *.user
6 | DesktopClient/bin/
7 | DesktopClient/obj/
8 | DesktopTest/obj/
9 | NativeLibrary/build/
10 | PortableClient/obj/
11 | TestClient/obj/
12 | ComLightLib/x64/
13 | NativeLibrary/x64/
14 | x64/
15 | ComLight/bin/
16 | DesktopTest/bin/
17 | ComLightLib/Win32/
18 | NativeLibrary/Win32/
19 | PortableClient/bin/
20 | Win32/
21 | Debug/
22 | packages/
23 | Demos/HelloWorldCs/obj/
24 | Demos/HelloWorldCs/bin/
25 | Demos/HelloWorldCpp/build/
26 | Demos/StreamsCS/obj/
27 | Demos/StreamsCS/bin/
28 | Demos/StreamsDesktop/obj/
29 | Demos/StreamsCpp/build/
30 | Demos/StreamsDesktop/bin/
--------------------------------------------------------------------------------
/ComLight/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using System.Runtime.InteropServices;
3 |
4 | [assembly: AssemblyTitle( "ComLight" )]
5 | [assembly: AssemblyDescription( "Lightweight cross-platform COM interop library for Windows and Linux. Allows to expose C++ objects to .NET, and .NET objects to C++." )]
6 | [assembly: AssemblyConfiguration( "" )]
7 | [assembly: AssemblyCompany( "" )]
8 | [assembly: AssemblyProduct( "ComLight" )]
9 | [assembly: AssemblyCopyright( "Copyright © const.me, 2019-2024" )]
10 | [assembly: AssemblyTrademark( "" )]
11 | [assembly: AssemblyCulture( "" )]
12 |
13 | [assembly: ComVisible( false )]
14 |
15 | [assembly: Guid( "16d6c3fb-2a8f-4134-a4b3-819411f2c595" )]
16 |
17 | [assembly: AssemblyVersion( "2.0.0.0" )]
18 | [assembly: AssemblyFileVersion( "2.0.0.0" )]
--------------------------------------------------------------------------------
/ComLight/Cache/Managed.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Diagnostics;
4 |
5 | namespace ComLight.Cache
6 | {
7 | /// Tracks live COM objects implemented in .NET.
8 | /// Despite interfaces inheritance, each ManagedObject instance builds it's own COM vtable.
9 | /// If you construct multiple wrappers around different COM interfaces implemented by the same .NET objects, the wrappers will be unrelated to each other, you can't even use QueryInterface(IID_IUnknown) trick to detect they're implemented by the same object.
10 | /// That's why we don't need multimaps for this one.
11 | static class Managed
12 | {
13 | static readonly object syncRoot = new object();
14 |
15 | /// COM objects constructed around C# objects
16 | static readonly Dictionary> managed = new Dictionary>();
17 |
18 | public static void add( IntPtr p, ManagedObject mo )
19 | {
20 | Debug.Assert( p != IntPtr.Zero );
21 |
22 | lock( syncRoot )
23 | {
24 | Debug.Assert( !managed.ContainsKey( p ) );
25 | managed.Add( p, new WeakReference( mo ) );
26 | }
27 | }
28 |
29 | public static bool drop( IntPtr p )
30 | {
31 | lock( syncRoot )
32 | {
33 | WeakReference wr;
34 | if( !managed.TryGetValue( p, out wr ) )
35 | return false;
36 | if( wr.isDead() )
37 | managed.Remove( p );
38 | // If the weak reference is alive, it means the COM pointer address was reused for another object.
39 | // The managedDrop is called by ManagedObject finalizer.
40 | // Finalizers run long after weak references expire: http://www.philosophicalgeek.com/2014/08/20/short-vs-long-weak-references-and-object-resurrection/
41 | // It's possible by the time finalizer is running, C++ code already constructed different object with the same COM pointer, and passed it to .NET.
42 | return true;
43 | }
44 | }
45 |
46 | public static ManagedObject lookup( IntPtr p )
47 | {
48 | WeakReference wr;
49 | lock( syncRoot )
50 | {
51 | if( !managed.TryGetValue( p, out wr ) )
52 | return null;
53 | }
54 | return wr.getTarget();
55 | }
56 |
57 | /// If `p` is the native COM pointer tracked by this class, call AddRef. Otherwise throw an exception.
58 | public static void addRef( IntPtr p )
59 | {
60 | lock( syncRoot )
61 | {
62 | var mo = managed.lookup( p )?.getTarget();
63 | if( null != mo )
64 | {
65 | mo.callAddRef();
66 | return;
67 | }
68 | }
69 | throw new ApplicationException( $"Native COM pointer { p.ToString( "X" ) } is not on the cache" );
70 | }
71 | }
72 | }
--------------------------------------------------------------------------------
/ComLight/Cache/Native.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Diagnostics;
4 | using System.Linq;
5 | using InterfacesMap = System.Runtime.CompilerServices.ConditionalWeakTable;
6 |
7 | namespace ComLight.Cache
8 | {
9 | /// Tracks live COM objects implemented in C++.
10 | /// Due to interfaces inheritance, the same IntPtr native pointer can be wrapped into multiple proxies. That's why we need a multimap here.
11 | static class Native
12 | {
13 | static readonly object syncRoot = new object();
14 |
15 | /// Array of COM interface types implemented by RuntimeClass proxy. We now support interfaces inheritance, a proxy may implement more than one.
16 | /// It's gonna be quite short anyway, most often just 1 interface, sometimes 2-3. That's why array instead of a hash map.
17 | static Type[] collectComInterfaces( RuntimeClass rc )
18 | {
19 | return rc.GetType().GetInterfaces()
20 | .Where( i => i.hasCustomAttribute() )
21 | .ToArray();
22 | }
23 |
24 | static readonly InterfacesMap.CreateValueCallback ifacesCallback = collectComInterfaces;
25 |
26 | static readonly Dictionary native = new Dictionary();
27 |
28 | public static void add( IntPtr p, RuntimeClass rc )
29 | {
30 | Debug.Assert( p != IntPtr.Zero );
31 |
32 | lock( syncRoot )
33 | {
34 | InterfacesMap map;
35 | if( !native.TryGetValue( p, out map ) )
36 | {
37 | map = new InterfacesMap();
38 | native.Add( p, map );
39 | }
40 | map.GetValue( rc, ifacesCallback );
41 | }
42 | }
43 |
44 | public static bool drop( IntPtr p, RuntimeClass rc )
45 | {
46 | lock( syncRoot )
47 | {
48 | if( native.TryGetValue( p, out InterfacesMap map ) )
49 | {
50 | bool removed = map.Remove( rc );
51 | if( !map.Any() )
52 | native.Remove( p );
53 | return removed;
54 | }
55 | return false;
56 | }
57 | }
58 |
59 | public static RuntimeClass lookup( IntPtr p, Type tInterface )
60 | {
61 | lock( syncRoot )
62 | {
63 | if( !native.TryGetValue( p, out InterfacesMap map ) )
64 | return null;
65 |
66 | foreach( var kvp in map )
67 | {
68 | if( !kvp.Key.isAlive() )
69 | continue;
70 |
71 | if( kvp.Value.Contains( tInterface ) )
72 | return kvp.Key;
73 | }
74 | return null;
75 | }
76 | }
77 | }
78 | }
--------------------------------------------------------------------------------
/ComLight/ComInterfaceAttribute.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace ComLight
4 | {
5 | /// Direction of the interface marshaling
6 | public enum eMarshalDirection: byte
7 | {
8 | /// Expose C++ objects to .NET
9 | ToManaged,
10 | /// Expose .NET objects to C++
11 | ToNative,
12 | /// Marshal objects both ways
13 | BothWays,
14 | }
15 |
16 | /// Attribute to mark COM interfaces, equivalent to [Guid( "..." ), InterfaceType( ComInterfaceType.InterfaceIsIUnknown )] in the desktop .NET COM interop.
17 | [AttributeUsage( AttributeTargets.Interface, Inherited = false )]
18 | public class ComInterfaceAttribute: Attribute
19 | {
20 | /// COM interface ID for this interface, must match the value in DEFINE_INTERFACE_ID macro in C++ code.
21 | public readonly Guid iid;
22 |
23 | /// You can limit the direction of the marshaling, e.g. it makes no sense to implement something like ID3D11Buffer in C#, it won't work.
24 | /// Single-direction marshaling is more efficient than the default .
25 | public readonly eMarshalDirection marshalDirection;
26 |
27 | /// Construct by parsing a string GUID
28 | public ComInterfaceAttribute( string iid, eMarshalDirection direction = eMarshalDirection.BothWays )
29 | {
30 | this.iid = Guid.Parse( iid );
31 | marshalDirection = direction;
32 | }
33 | }
34 | }
--------------------------------------------------------------------------------
/ComLight/ComLight.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 | Copyright © const.me, 2019
6 | https://github.com/Const-me/ComLightInterop
7 | const.me
8 | true
9 | ComLightInterop
10 | ComLightInterop.nuspec
11 | false
12 |
13 |
14 |
15 | true
16 |
17 |
18 |
19 |
20 | TextTemplatingFileGenerator
21 | ErrorCodez.cs
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | True
32 | True
33 | ErrorCodez.tt
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/ComLight/ComLightCast.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Reflection;
3 |
4 | namespace ComLight
5 | {
6 | /// Utility class to cast objects between interfaces
7 | public static class ComLightCast
8 | {
9 | /// Cast the object to another COM interface
10 | /// Result COM interface
11 | /// The object to cast
12 | /// If true, and the argument is a C++ implemented object, this method will release the old one.
13 | /// The object casted to the requested COM interface
14 | public static I cast( object obj, bool releaseOldOne = false ) where I : class
15 | {
16 | var type = typeof( I );
17 | if( !type.IsInterface )
18 | throw new InvalidCastException( "The type argument of the cast method must be an interface" );
19 | ComInterfaceAttribute attribute = type.GetCustomAttribute();
20 | if( null == attribute )
21 | throw new InvalidCastException( "The type argument of the cast method must be an interface with [ComInterface] custom attribute" );
22 |
23 | if( null == obj )
24 | return null;
25 |
26 | if( obj is RuntimeClass runtimeClass )
27 | {
28 | // C++ implemented COM object
29 | if( obj is I result )
30 | {
31 | // The proxy already implements the interface. We're probably casting to a base interface.
32 | return result;
33 | }
34 |
35 | IntPtr newPointer;
36 | try
37 | {
38 | newPointer = runtimeClass.queryInterface( attribute.iid, true );
39 | }
40 | catch( Exception ex )
41 | {
42 | throw new InvalidCastException( "Unable to cast, the native object doesn't support the interface", ex );
43 | }
44 | finally
45 | {
46 | if( releaseOldOne )
47 | ( (IDisposable)runtimeClass ).Dispose();
48 | }
49 |
50 | try
51 | {
52 | return (I)NativeWrapper.wrap( type, newPointer );
53 | }
54 | catch( Exception ex )
55 | {
56 | runtimeClass.release();
57 | throw new InvalidCastException( "Unable to cast, something's wrong with the interface", ex );
58 | }
59 | }
60 |
61 | if( obj is ManagedObject managedObject )
62 | {
63 | // C# implemented COM object
64 | if( managedObject.managed is I result )
65 | return result;
66 |
67 | throw new InvalidCastException( $"{ managedObject.managed.GetType().FullName } doesn't implement interface { type.FullName }" );
68 | }
69 |
70 | throw new InvalidCastException( $"{ obj.GetType().FullName } is not a ComLight object" );
71 | }
72 | }
73 | }
--------------------------------------------------------------------------------
/ComLight/ComLightInterop.nuspec:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | ComLightInterop
5 | 2.0.0
6 | const.me
7 | const.me
8 | Lightweight cross-platform COM interop library for Windows and Linux. Allows to expose C++ objects to .NET, and .NET objects to C++.
9 | The library only supports IUnknown-based interfaces, it doesn’t handle IDispatch.
10 | You can only use simple types in your interfaces: primitives, structures, strings, pointers, function pointers, but not VARIANT or SAFEARRAY.
11 | This package targets 3 platforms, .NET framework 4.7.2, .NET 8.0, and VC++.
12 | Unfortunately, VC++ is Windows only.
13 | To build Linux shared libraries implementing or consuming COM objects, please add "build/native" directory from this package to C++ include paths.
14 | For cmake see include_directories command, or use some other method, depending on your C++ build system, and compiler.
15 | Keep in mind .NET assemblies are often “AnyCPU”, C++ libraries are not, please make sure you’re building your native code for the correct architecture.
16 | docs\Readme.md
17 | Copyright © const.me, 2019-2024
18 | Lightweight cross-platform COM interop
19 |
20 | Upgraded .NET runtime to 8.0. The final version which supports older versions of .NET Core runtime is 1.3.8.
21 |
22 | In addition to .NET 8, the current version of the library fully supports legacy .NET framework 4.7.2 or newer.
23 |
24 | Bugfix, C# objects passed to C++ are protected from GC for the duration of the C++ call, using `GC.KeepAlive` in the runtime-generated code.
25 | https://github.com/Const-me/ComLightInterop
26 |
27 | MIT
28 | false
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 | native, ComLightInterop, COM
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
--------------------------------------------------------------------------------
/ComLight/ComLightInterop.targets:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | $(MSBuildThisFileDirectory)native\;%(AdditionalIncludeDirectories)
7 |
8 |
9 |
10 |
11 |
12 | NotUsing
13 |
14 |
15 |
--------------------------------------------------------------------------------
/ComLight/CustomConventionsAttribute.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Reflection;
3 |
4 | namespace ComLight
5 | {
6 | /// Apply to COM interface to use custom prologue function, and custom errors marshaling.
7 | /// Protip: C++ doesn’t feature async-await. You can keep per-thread error context in a static variable marked with [ThreadStatic] attribute.
8 | [AttributeUsage( AttributeTargets.Interface, Inherited = false )]
9 | public class CustomConventionsAttribute: Attribute
10 | {
11 | /// This static method will be called immediately before every native call.
12 | public readonly MethodInfo prologue = null;
13 |
14 | /// This static method will be used to convert HRESULT codes into .NET exceptions. It should throw appropriate exceptions for FAILED codes, and do nothing if the code is SUCCEEDED.
15 | public readonly MethodInfo throwException = null;
16 |
17 | /// This static method will be used to convert HRESULT codes for COM methods which return booleans. It should throw exceptions for FAILED codes, return true for S_OK, return false for anything else.
18 | public readonly MethodInfo throwAndReturnBool = null;
19 |
20 | /// Construct with the type implementing the conventions.
21 | public CustomConventionsAttribute( Type type )
22 | {
23 | if( !type.IsPublic )
24 | throw new ArgumentException( $"The type { type.FullName } specified in [ CustomConventions ] attribute ain’t public." );
25 |
26 | var mi = type.GetMethod( "prologue", BindingFlags.Public | BindingFlags.Static, null, MiscUtils.noTypes, null );
27 | if( null != mi )
28 | {
29 | if( mi.ReturnType != typeof( void ) )
30 | throw new ApplicationException( $"The { type.FullName }.prologue() method returns something, it must be void." );
31 | prologue = mi;
32 | }
33 |
34 | Type[] it = new Type[ 1 ] { typeof( int ) };
35 | mi = type.GetMethod( "throwForHR", BindingFlags.Public | BindingFlags.Static, null, it, null );
36 | if( null != mi )
37 | {
38 | if( mi.ReturnType != typeof( void ) )
39 | throw new ApplicationException( $"The { type.FullName }.throwForHR() method returns something, it must be void." );
40 | throwException = mi;
41 | }
42 |
43 | mi = type.GetMethod( "throwAndReturnBool", BindingFlags.Public | BindingFlags.Static, null, it, null );
44 | if( null != mi )
45 | {
46 | if( mi.ReturnType != typeof( bool ) )
47 | throw new ApplicationException( $"The { type.FullName }.throwAndReturnBool() method must return bool." );
48 | throwAndReturnBool = mi;
49 | }
50 | }
51 | }
52 | }
--------------------------------------------------------------------------------
/ComLight/DebuggerTypeProxyAttribute.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace ComLight
4 | {
5 | /// Apply this attribute to COM interfaces to emit on the proxies which wrap C++ objects for .NET
6 | /// This attribute only affects Visual Studio debugger, shouldn't affect runtime performance.
7 | /// The type specified in constructor should have a public constructor which accepts the type of the interface.
8 | [AttributeUsage( AttributeTargets.Interface, AllowMultiple = false )]
9 | public sealed class DebuggerTypeProxyAttribute: Attribute
10 | {
11 | internal readonly Type type;
12 | /// Initializes a new instance of the DebuggerTypeProxyAttribute class using the type of the proxy.
13 | public DebuggerTypeProxyAttribute( Type type ) =>
14 | this.type = type ?? throw new ArgumentNullException( nameof( type ) );
15 | }
16 | }
--------------------------------------------------------------------------------
/ComLight/Emit/Assembly.cs:
--------------------------------------------------------------------------------
1 | // Uncomment the following line if you want the dynamic assembly to be saved. Only works on desktop .NET framework.
2 | // #define DBG_SAVE_DYNAMIC_ASSEMBLY
3 | using System.Reflection;
4 | using System.Reflection.Emit;
5 | using System;
6 |
7 | namespace ComLight.Emit
8 | {
9 | ///
10 | static class Assembly
11 | {
12 | public static readonly AssemblyBuilder assemblyBuilder;
13 | public static readonly ModuleBuilder moduleBuilder;
14 |
15 | static Assembly()
16 | {
17 | // Create dynamic assembly builder, and cache some reflected stuff we use to build these proxies in runtime.
18 | var an = new AssemblyName( "ComLight.Wrappers" );
19 |
20 | #if NETCOREAPP || !DBG_SAVE_DYNAMIC_ASSEMBLY
21 | assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly( an, AssemblyBuilderAccess.Run );
22 | moduleBuilder = assemblyBuilder.DefineDynamicModule( "MainModule" );
23 | #else
24 | AssemblyBuilderAccess aba = AssemblyBuilderAccess.RunAndSave;
25 | assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly( an, aba );
26 | moduleBuilder = assemblyBuilder.DefineDynamicModule( "MainModule", an.Name + ".dll" );
27 |
28 | Action actSave = () =>
29 | {
30 | string name = an.Name + ".dll";
31 | try
32 | {
33 | assemblyBuilder.Save( name );
34 | }
35 | catch( Exception ex )
36 | {
37 | Console.WriteLine( "Error saving the assembly: {0}", ex.Message );
38 | }
39 | };
40 | AppDomain.CurrentDomain.UnhandledException += ( object sender, UnhandledExceptionEventArgs e ) =>
41 | {
42 | actSave();
43 | };
44 | AppDomain.CurrentDomain.ProcessExit += ( object sender, EventArgs e ) =>
45 | {
46 | actSave();
47 | };
48 | #endif
49 | }
50 | }
51 | }
--------------------------------------------------------------------------------
/ComLight/Emit/BaseInterfaces.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Reflection;
5 | using System.Reflection.Emit;
6 |
7 | namespace ComLight.Emit
8 | {
9 | sealed class BaseInterfaces
10 | {
11 | readonly Type tInterface;
12 | readonly TypeBuilder typeBuilder;
13 | readonly Dictionary baseMethods;
14 |
15 | BaseInterfaces( TypeBuilder typeBuilder, Type tInterface, IEnumerable baseInterfaces )
16 | {
17 | this.tInterface = tInterface;
18 | this.typeBuilder = typeBuilder;
19 | baseMethods = baseInterfaces
20 | .SelectMany( i => i.getMethodsWithoutProperties() )
21 | .GroupBy( m => m.Name )
22 | .ToDictionary( mg => mg.Key, mg => mg.ToArray() );
23 | }
24 |
25 | public static BaseInterfaces createIfNeeded( TypeBuilder typeBuilder, Type tInterface )
26 | {
27 | var bases = tInterface.GetInterfaces();
28 | if( bases.isEmpty() )
29 | return null;
30 | IEnumerable excludeDisposable = bases.Where( t => t != typeof( IDisposable ) );
31 | if( excludeDisposable.Any() )
32 | return new BaseInterfaces( typeBuilder, tInterface, excludeDisposable );
33 | return null;
34 | }
35 |
36 | public void implementedMethod( MethodBuilder newMethod, string name )
37 | {
38 | MethodInfo[] methods = baseMethods.lookup( name );
39 | if( methods.isEmpty() )
40 | return;
41 | foreach( var bm in methods )
42 | typeBuilder.DefineMethodOverride( newMethod, bm );
43 | }
44 | }
45 | }
--------------------------------------------------------------------------------
/ComLight/Emit/DelegatesBuilder.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Reflection;
4 | using System.Reflection.Emit;
5 |
6 | namespace ComLight.Emit
7 | {
8 | /// A wrapper around which assigns unique names to the delegates.
9 | /// Both C# and C++ allow interface methods to have the same name, distinguished by different argument types.
10 | sealed class DelegatesBuilder
11 | {
12 | public readonly TypeBuilder typeBuilder;
13 | readonly HashSet typeNames = new HashSet();
14 |
15 | public DelegatesBuilder( string name )
16 | {
17 | typeBuilder = Assembly.moduleBuilder.emitStaticClass( name );
18 | }
19 |
20 | string assignName( MethodInfo comMethod )
21 | {
22 | if( typeNames.Add( comMethod.Name ) )
23 | return comMethod.Name;
24 | string alt = comMethod.Name + comMethod.GetHashCode().ToString( "x" );
25 | if( typeNames.Add( alt ) )
26 | return alt;
27 | throw new ApplicationException( $"NativeDelegatesBuilder unable to assign unique name for a method { comMethod.DeclaringType.FullName }.{ comMethod.Name }" );
28 | }
29 |
30 | public TypeBuilder defineMulticastDelegate( MethodInfo comMethod )
31 | {
32 | string name = assignName( comMethod );
33 | TypeAttributes ta = TypeAttributes.AutoClass | TypeAttributes.AnsiClass | TypeAttributes.Sealed | TypeAttributes.NestedPublic;
34 | return typeBuilder.DefineNestedType( name, ta, typeof( MulticastDelegate ) );
35 | }
36 |
37 | /// Creates a System.Type object for the class. After defining fields and methods on the class, CreateType is called in order to load its Type object.
38 | public Type createType()
39 | {
40 | return typeBuilder.CreateType();
41 | }
42 | }
43 | }
--------------------------------------------------------------------------------
/ComLight/Emit/NativeDelegates.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Diagnostics;
4 | using System.Linq;
5 | using System.Reflection;
6 | using System.Reflection.Emit;
7 | using System.Runtime.InteropServices;
8 |
9 | namespace ComLight.Emit
10 | {
11 | /// Build static class with the native function pointer delegates
12 | static class NativeDelegates
13 | {
14 | /// Constructor of UnmanagedFunctionPointerAttribute
15 | static readonly ConstructorInfo ciFPAttribute;
16 |
17 | static NativeDelegates()
18 | {
19 | Type tPointerAttr = typeof( UnmanagedFunctionPointerAttribute );
20 | ciFPAttribute = tPointerAttr.GetConstructor( new Type[ 1 ] { typeof( CallingConvention ) } );
21 | }
22 |
23 | /// Create static class with the delegates
24 | static TypeBuilder createDelegatesType( Type tInterface )
25 | {
26 | return Assembly.moduleBuilder.emitStaticClass( tInterface.FullName + "_native" );
27 | }
28 |
29 | static Type nativeRetValArgType( MethodInfo method )
30 | {
31 | Type tRet = method.ReturnType;
32 | if( tRet.IsValueType )
33 | return tRet.MakeByRefType();
34 |
35 | Debug.Assert( tRet.hasCustomAttribute() );
36 | return MiscUtils.intPtrRef;
37 | }
38 |
39 | static Type createDelegate( DelegatesBuilder builder, MethodInfo method )
40 | {
41 | // Initially based on this: https://blogs.msdn.microsoft.com/joelpob/2004/02/15/creating-delegate-types-via-reflection-emit/
42 |
43 | // Create the delegate type
44 | TypeBuilder tb = builder.defineMulticastDelegate( method );
45 |
46 | // Apply [UnmanagedFunctionPointer] using the value from RuntimeClass.defaultCallingConvention
47 | CustomAttributeBuilder cab = new CustomAttributeBuilder( ciFPAttribute, new object[ 1 ] { RuntimeClass.defaultCallingConvention } );
48 | tb.SetCustomAttribute( cab );
49 |
50 | // Create constructor for the delegate
51 | MethodAttributes ma = MethodAttributes.SpecialName | MethodAttributes.RTSpecialName | MethodAttributes.HideBySig | MethodAttributes.Public;
52 | ConstructorBuilder cb = tb.DefineConstructor( ma, CallingConventions.Standard, new Type[] { typeof( object ), typeof( IntPtr ) } );
53 | cb.SetImplementationFlags( MethodImplAttributes.Runtime | MethodImplAttributes.Managed );
54 | cb.DefineParameter( 1, ParameterAttributes.In, "object" );
55 | cb.DefineParameter( 2, ParameterAttributes.In, "method" );
56 |
57 | // Create Invoke method for the delegate. Appending one more parameter to the start, `[in] IntPtr pThis`
58 | ParameterInfo[] methodParams = method.GetParameters();
59 | int nativeParamsCount = methodParams.Length + 1;
60 | int retValIndex = -1;
61 | RetValIndexAttribute rvi = method.GetCustomAttribute();
62 | if( rvi != null )
63 | {
64 | retValIndex = rvi.index;
65 | nativeParamsCount++;
66 | }
67 |
68 | Type[] paramTypes = new Type[ nativeParamsCount ];
69 | paramTypes[ 0 ] = typeof( IntPtr );
70 | int iNativeParam = 1;
71 | for( int i = 0; i < methodParams.Length; i++, iNativeParam++ )
72 | {
73 | if( i == retValIndex )
74 | {
75 | retValIndex = -1;
76 | i--;
77 | paramTypes[ iNativeParam ] = nativeRetValArgType( method );
78 | continue;
79 | }
80 |
81 | ParameterInfo pi = methodParams[ i ];
82 | Type tp = pi.ParameterType;
83 | iCustomMarshal cm = pi.customMarshaller();
84 | if( null != cm )
85 | tp = cm.getNativeType( pi );
86 | paramTypes[ iNativeParam ] = tp;
87 | }
88 | if( retValIndex >= 0 )
89 | {
90 | // User has specified [RetValIndex] value after the rest of the parameters
91 | paramTypes[ iNativeParam ] = nativeRetValArgType( method );
92 | }
93 |
94 | Type returnType;
95 | if( method.ReturnType != typeof( IntPtr ) || null != rvi )
96 | returnType = typeof( int );
97 | else
98 | returnType = typeof( IntPtr );
99 |
100 | var mb = tb.DefineMethod( "Invoke", MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.NewSlot | MethodAttributes.Virtual, returnType, paramTypes );
101 | mb.SetImplementationFlags( MethodImplAttributes.Runtime | MethodImplAttributes.Managed );
102 |
103 | mb.DefineParameter( 1, ParameterAttributes.In, "pThis" );
104 |
105 | iNativeParam = 2; // 2 because the first one is native this pointer, and MethodBuilder.DefineParameter API uses 1-based indices, the number 0 represents the return value of the method.
106 | retValIndex = rvi?.index ?? -1;
107 | for( int i = 0; i < methodParams.Length; i++, iNativeParam++ )
108 | {
109 | if( i == retValIndex )
110 | {
111 | retValIndex = -1;
112 | i--;
113 | mb.DefineParameter( iNativeParam, ParameterAttributes.Out, "retVal" );
114 | continue;
115 | }
116 | ParameterInfo pi = methodParams[ i ];
117 | ParameterBuilder pb = mb.DefineParameter( iNativeParam, pi.Attributes, pi.Name );
118 | ParamsMarshalling.buildDelegateParam( pi, pb, rvi?.index );
119 | }
120 | if( retValIndex >= 0 )
121 | {
122 | // User has specified [RetValIndex] value after the rest of the parameters
123 | mb.DefineParameter( iNativeParam, ParameterAttributes.Out, "retVal" );
124 | }
125 |
126 | // The method has no code, it's pure virtual.
127 | return tb.CreateType();
128 | }
129 |
130 | static readonly object syncRoot = new object();
131 | static readonly Dictionary delegatesCache = new Dictionary();
132 |
133 | /// Build static class with the native function pointer delegates, return array of delegate types.
134 | /// It caches the results because the delegate types are used for both directions of the interop.
135 | public static Type[] buildDelegates( Type tInterface )
136 | {
137 | lock( syncRoot )
138 | {
139 | Type[] result;
140 | if( delegatesCache.TryGetValue( tInterface, out result ) )
141 | return result;
142 |
143 | var tbDelegates = new DelegatesBuilder( tInterface.FullName + "_native" );
144 | // Add delegate types per method
145 | result = tInterface.getMethodsWithoutProperties().Select( mi => createDelegate( tbDelegates, mi ) ).ToArray();
146 | tbDelegates.createType();
147 | delegatesCache.Add( tInterface, result );
148 |
149 | return result;
150 | }
151 | }
152 | }
153 | }
--------------------------------------------------------------------------------
/ComLight/Emit/PropertiesBuilder.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Reflection;
4 | using System.Reflection.Emit;
5 |
6 | namespace ComLight.Emit
7 | {
8 | sealed class PropertiesBuilder
9 | {
10 | static readonly IEqualityComparer namesComparer = StringComparer.InvariantCultureIgnoreCase;
11 |
12 | struct MethodNames
13 | {
14 | public readonly string getterMethod, setterMethod;
15 |
16 | public MethodNames( PropertyInfo pi )
17 | {
18 | var attrib = pi.GetCustomAttribute();
19 | if( null == attrib )
20 | {
21 | getterMethod = "get" + pi.Name;
22 | setterMethod = "set" + pi.Name;
23 | }
24 | else
25 | {
26 | getterMethod = attrib.getterMethod;
27 | setterMethod = attrib.setterMethod;
28 | }
29 | }
30 | }
31 |
32 | static void addMethod( ref Dictionary result, string key, MethodInfo val, Type ifaceBuild )
33 | {
34 | if( null == result )
35 | {
36 | result = new Dictionary( namesComparer );
37 | result.Add( key, val );
38 | return;
39 | }
40 |
41 | if( result.ContainsKey( key ) )
42 | {
43 | throw new ArgumentException( $"COM interface { ifaceBuild.FullName } has multiple properties implemented by the same method { key }. This is not supported." );
44 | // It's easy to support BTW, but I don't see any good reason to.
45 | // Why would you want different properties doing exactly same thing?
46 | }
47 | result.Add( key, val );
48 | }
49 |
50 | static void reflect( ref Dictionary result, Type ifaceReflect, Type ifaceBuild )
51 | {
52 | PropertyInfo[] properties = ifaceReflect.GetProperties();
53 | foreach( var pi in properties )
54 | {
55 | MethodNames names = new MethodNames( pi );
56 | MethodInfo getter = pi.GetGetMethod(), setter = pi.GetSetMethod();
57 | if( null != getter )
58 | addMethod( ref result, names.getterMethod, getter, ifaceBuild );
59 | if( null != setter )
60 | addMethod( ref result, names.setterMethod, setter, ifaceBuild );
61 | }
62 | }
63 |
64 | // Key = COM method name, value = getters or setter which need implementing.
65 | readonly Dictionary dict;
66 |
67 | public static PropertiesBuilder createIfNeeded( Type tInterface )
68 | {
69 | Dictionary dict = null;
70 |
71 | reflect( ref dict, tInterface, tInterface );
72 |
73 | foreach( Type baseIface in tInterface.GetInterfaces() )
74 | reflect( ref dict, baseIface, tInterface );
75 |
76 | if( null == dict )
77 | return null;
78 |
79 | return new PropertiesBuilder( dict );
80 | }
81 |
82 | PropertiesBuilder( Dictionary dict )
83 | {
84 | this.dict = dict;
85 | }
86 |
87 | public void implement( TypeBuilder typeBuilder, MethodInfo comMethod, MethodBuilder comMethodBuilder )
88 | {
89 | if( !dict.TryGetValue( comMethod.Name, out var mi ) )
90 | return;
91 | implementProperty( typeBuilder, comMethod, comMethodBuilder, mi );
92 | }
93 |
94 | const MethodAttributes methodAttributes = MethodAttributes.Private | MethodAttributes.HideBySig | MethodAttributes.SpecialName | MethodAttributes.NewSlot | MethodAttributes.Virtual | MethodAttributes.Final;
95 |
96 | void implementProperty( TypeBuilder typeBuilder, MethodInfo comMethod, MethodBuilder methodBuilder, MethodInfo propertyMethod )
97 | {
98 | var mp = comMethod.GetParameters();
99 |
100 | if( propertyMethod.Name.StartsWith( "get_" ) )
101 | {
102 | // Implement property getter
103 |
104 | if( mp.Length == 0 )
105 | {
106 | // The COM method doesn't accept any parameters.
107 | // We don't need to build any extra methods.
108 | if( comMethod.ReturnType != propertyMethod.ReturnType )
109 | throw new ArgumentException( $"Property getter { propertyMethod.Name } has return type { propertyMethod.ReturnType.FullName }, while the COM method { comMethod.Name } returns { comMethod.ReturnType.FullName }. They must be the same." );
110 | typeBuilder.DefineMethodOverride( methodBuilder, propertyMethod );
111 | return;
112 | }
113 |
114 | // The COM method has parameters. It must be exactly one then, output.
115 | // Build a small getter method with 1 local variable.
116 | if( mp.Length != 1 || !mp[ 0 ].IsOut )
117 | throw new ArgumentException( $"COM method { comMethod.Name } can't implement { propertyMethod.Name }, the COM method must have exactly 1 parameter, output one." );
118 | if( mp[ 0 ].ParameterType != propertyMethod.ReturnType.MakeByRefType() )
119 | throw new ArgumentException( $"COM method { comMethod.Name } can't implement { propertyMethod.Name }, the types are different." );
120 |
121 | MethodBuilder mb = typeBuilder.DefineMethod( propertyMethod.Name, methodAttributes, propertyMethod.ReturnType, MiscUtils.noTypes );
122 | ILGenerator il = mb.GetILGenerator();
123 | LocalBuilder res = il.DeclareLocal( propertyMethod.ReturnType );
124 | il.Emit( OpCodes.Ldarg_0 );
125 | il.Emit( OpCodes.Ldloca_S, res );
126 | il.Emit( OpCodes.Call, methodBuilder );
127 | // CLR return values through the stack. COM methods may return things, the "pop" discards the return value.
128 | if( comMethod.ReturnType != typeof( void ) )
129 | il.Emit( OpCodes.Pop );
130 | il.Emit( OpCodes.Ldloc_0 );
131 | il.Emit( OpCodes.Ret );
132 |
133 | typeBuilder.DefineMethodOverride( mb, propertyMethod );
134 | return;
135 | }
136 |
137 | if( propertyMethod.Name.StartsWith( "set_" ) )
138 | {
139 | // Implement property setter
140 |
141 | if( mp.Length != 1 )
142 | throw new ArgumentException( $"COM method { comMethod.Name } can't implement { propertyMethod.Name }, the COM method must take a single argument" );
143 |
144 | ParameterInfo piProperty = propertyMethod.GetParameters()[ 0 ];
145 | if( mp[ 0 ].ParameterType == piProperty.ParameterType )
146 | {
147 | // Parameter types match. We don't need to build any extra methods.
148 | typeBuilder.DefineMethodOverride( methodBuilder, propertyMethod );
149 | return;
150 | }
151 |
152 | // The COM method is like void setSomething( [In] ref something )
153 | // Build a small setter method with slightly different signature, without the `ref`
154 | if( mp[ 0 ].ParameterType != piProperty.ParameterType.MakeByRefType() )
155 | throw new ArgumentException( $"COM method { comMethod.Name } can't implement { propertyMethod.Name }, the types are different." );
156 |
157 | MethodBuilder mb = typeBuilder.DefineMethod( propertyMethod.Name, methodAttributes, typeof( void ), new Type[ 1 ] { piProperty.ParameterType } );
158 | mb.DefineParameter( 1, ParameterAttributes.In, "value" );
159 |
160 | ILGenerator il = mb.GetILGenerator();
161 | il.Emit( OpCodes.Ldarg_0 );
162 | il.Emit( OpCodes.Ldarga_S, (byte)0 );
163 | il.Emit( OpCodes.Call, methodBuilder );
164 | // CLR return values through the stack. COM methods may return things, the "pop" discards the return value.
165 | if( comMethod.ReturnType != typeof( void ) )
166 | il.Emit( OpCodes.Pop );
167 | il.Emit( OpCodes.Ret );
168 |
169 | typeBuilder.DefineMethodOverride( mb, propertyMethod );
170 | return;
171 | }
172 |
173 | throw new ArgumentException( "Unexpected property method " + propertyMethod.Name );
174 | }
175 | }
176 | }
--------------------------------------------------------------------------------
/ComLight/Emit/Proxy.standard.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Reflection;
3 | using System.Reflection.Emit;
4 |
5 | namespace ComLight.Emit
6 | {
7 | static partial class Proxy
8 | {
9 | /// A method without custom marshaling. The generated code calls native delegate directly.
10 | /// The native delegate field is initialized in the constructor of the proxy.
11 | sealed class ProxyMethod: iMethodPrefab
12 | {
13 | readonly MethodInfo method;
14 | readonly Type tNativeDelegate;
15 |
16 | public ProxyMethod( MethodInfo mi, Type tNativeDelegate )
17 | {
18 | method = mi;
19 | this.tNativeDelegate = tNativeDelegate;
20 | }
21 |
22 | FieldBuilder iMethodPrefab.emitField( TypeBuilder tb )
23 | {
24 | return tb.DefineField( "m_" + method.Name, tNativeDelegate, privateReadonly );
25 | }
26 |
27 | // This version builds native delegate in constructor, doesn't need extra arguments.
28 | Type iMethodPrefab.tCtorArg => null;
29 |
30 | void iMethodPrefab.emitConstructorBody( ILGenerator il, int methodIndex, ref int ctorArgIndex, FieldBuilder field )
31 | {
32 | il.Emit( OpCodes.Ldarg_0 );
33 | il.Emit( OpCodes.Ldarg_2 );
34 | il.pushIntConstant( methodIndex + 3 );
35 | il.Emit( OpCodes.Ldelem_I );
36 | // Specialize the generic Marshal.GetDelegateForFunctionPointer method with the delegate type
37 | MethodInfo mi = miGetDelegate.MakeGenericMethod( tNativeDelegate );
38 | il.Emit( OpCodes.Call, mi );
39 | // Store delegate in the readonly field
40 | il.Emit( OpCodes.Stfld, field );
41 | }
42 |
43 | void iMethodPrefab.emitMethod( MethodBuilder mb, FieldBuilder field, CustomConventionsAttribute customConventions )
44 | {
45 | ParameterInfo[] parameters = method.GetParameters();
46 |
47 | // Method body
48 | ILGenerator il = mb.GetILGenerator();
49 |
50 | var prologue = customConventions?.prologue;
51 | if( null != prologue )
52 | il.EmitCall( OpCodes.Call, prologue, null );
53 |
54 | // Load the delegate from this
55 | il.Emit( OpCodes.Ldarg_0 );
56 | il.Emit( OpCodes.Ldfld, field );
57 | // Load nativePointer from the base class
58 | il.Emit( OpCodes.Ldarg_0 );
59 | il.Emit( OpCodes.Ldfld, fiNativePointer );
60 | // Load arguments
61 | for( int i = 0; i < parameters.Length; i++ )
62 | il.loadArg( i + 1 );
63 | // Call the delegate
64 | MethodInfo invoke = field.FieldType.GetMethod( "Invoke" );
65 | il.Emit( OpCodes.Callvirt, invoke );
66 |
67 | if( method.ReturnType == typeof( void ) )
68 | {
69 | MethodInfo mi = customConventions?.throwException ?? miThrow;
70 | // Call ErrorCodes.throwForHR
71 | il.EmitCall( OpCodes.Call, mi, null );
72 | }
73 | else if( method.ReturnType == typeof( bool ) )
74 | {
75 | MethodInfo mi = customConventions?.throwAndReturnBool ?? miThrowRetBool;
76 | il.EmitCall( OpCodes.Call, mi, null );
77 | }
78 |
79 | il.Emit( OpCodes.Ret );
80 | }
81 | }
82 | }
83 | }
--------------------------------------------------------------------------------
/ComLight/IO/ManagedReadStream.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 |
4 | namespace ComLight.IO
5 | {
6 | /// Implement .NET readonly stream on top of native iReadStream
7 | class ManagedReadStream: Stream
8 | {
9 | readonly IntPtr com;
10 | readonly iReadStream native;
11 |
12 | ManagedReadStream( IntPtr com, iReadStream native )
13 | {
14 | this.com = com;
15 | this.native = native;
16 | }
17 | ~ManagedReadStream()
18 | {
19 | cache.dropIfDead( com );
20 | }
21 |
22 | public override bool CanRead => true;
23 |
24 | public override bool CanSeek => true;
25 |
26 | public override bool CanWrite => false;
27 |
28 | public override long Length
29 | {
30 | get
31 | {
32 | native.getLength( out long res );
33 | return res;
34 | }
35 | }
36 |
37 | public override long Position
38 | {
39 | get
40 | {
41 | native.getPosition( out long pos );
42 | return pos;
43 | }
44 | set
45 | {
46 | Seek( value, SeekOrigin.Begin );
47 | }
48 | }
49 |
50 | public override void Flush()
51 | {
52 | throw new NotSupportedException();
53 | }
54 |
55 | public override int Read( byte[] buffer, int offset, int count )
56 | {
57 | var span = new Span( buffer, offset, count );
58 | int cbRead;
59 | native.read( ref span.GetPinnableReference(), count, out cbRead );
60 | return cbRead;
61 | }
62 |
63 | public override long Seek( long offset, SeekOrigin origin )
64 | {
65 | eSeekOrigin so = (eSeekOrigin)(byte)origin;
66 | native.seek( offset, so );
67 | return Position;
68 | }
69 |
70 | public override void SetLength( long value )
71 | {
72 | throw new NotSupportedException();
73 | }
74 |
75 | public override void Write( byte[] buffer, int offset, int count )
76 | {
77 | throw new NotSupportedException();
78 | }
79 |
80 | static ManagedReadStream factory( IntPtr nativeComPointer )
81 | {
82 | iReadStream irs = NativeWrapper.wrap( nativeComPointer );
83 | return new ManagedReadStream( nativeComPointer, irs );
84 | }
85 | static readonly NativeWrapperCache cache = new NativeWrapperCache( factory );
86 |
87 | /// Class factory method
88 | public static Stream create( IntPtr nativeComPointer )
89 | {
90 | return cache.wrap( nativeComPointer );
91 | }
92 | }
93 | }
--------------------------------------------------------------------------------
/ComLight/IO/ManagedWriteStream.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 |
4 | namespace ComLight.IO
5 | {
6 | /// Implement .NET write only stream on top of native iWriteStream
7 | class ManagedWriteStream: Stream
8 | {
9 | readonly IntPtr com;
10 | readonly iWriteStream native;
11 |
12 | ManagedWriteStream( IntPtr com, iWriteStream native )
13 | {
14 | this.com = com;
15 | this.native = native;
16 | }
17 | ~ManagedWriteStream()
18 | {
19 | cache.dropIfDead( com );
20 | }
21 |
22 | public override bool CanRead => false;
23 |
24 | public override bool CanSeek => false;
25 |
26 | public override bool CanWrite => true;
27 |
28 | public override long Length => throw new NotSupportedException();
29 |
30 | public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); }
31 |
32 | public override void Flush()
33 | {
34 | native.flush();
35 | }
36 |
37 | public override int Read( byte[] buffer, int offset, int count )
38 | {
39 | throw new NotSupportedException();
40 | }
41 |
42 | public override long Seek( long offset, SeekOrigin origin )
43 | {
44 | throw new NotSupportedException();
45 | }
46 |
47 | public override void SetLength( long value )
48 | {
49 | throw new NotSupportedException();
50 | }
51 |
52 | public override void Write( byte[] buffer, int offset, int count )
53 | {
54 | // var span = new ReadOnlySpan( buffer, offset, count );
55 | // Can't use ReadOnlySpan due to API inconsistency, there's no ref readonly arguments, only ref readonly returns
56 |
57 | var span = new Span( buffer, offset, count );
58 | native.write( ref span.GetPinnableReference(), count );
59 | }
60 |
61 | static ManagedWriteStream factory( IntPtr nativeComPointer )
62 | {
63 | iWriteStream iws = NativeWrapper.wrap( nativeComPointer );
64 | return new ManagedWriteStream( nativeComPointer, iws );
65 | }
66 | static readonly NativeWrapperCache cache = new NativeWrapperCache( factory );
67 |
68 | /// Class factory method
69 | public static Stream create( IntPtr nativeComPointer )
70 | {
71 | return cache.wrap( nativeComPointer );
72 | }
73 | }
74 | }
--------------------------------------------------------------------------------
/ComLight/IO/NativeReadStream.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Runtime.CompilerServices;
4 | using System.Runtime.InteropServices;
5 |
6 | namespace ComLight.IO
7 | {
8 | /// Wraps .NET stream into native iReadStream
9 | class NativeReadStream: iReadStream, IDisposable, iComDisposable
10 | {
11 | readonly Stream stream;
12 |
13 | NativeReadStream( Stream stream )
14 | {
15 | this.stream = stream;
16 | }
17 |
18 | void iReadStream.getLength( out long length )
19 | {
20 | length = stream.Length;
21 | }
22 |
23 | #if !NETCOREAPP
24 | unsafe
25 | #endif
26 | void iReadStream.read( ref byte lpBuffer, int nNumberOfBytesToRead, out int lpNumberOfBytesRead )
27 | {
28 | #if NETCOREAPP
29 | var span = MemoryMarshal.CreateSpan( ref lpBuffer, nNumberOfBytesToRead );
30 | #else
31 | var span = new Span( Unsafe.AsPointer( ref lpBuffer ), nNumberOfBytesToRead );
32 | #endif
33 | lpNumberOfBytesRead = stream.Read( span );
34 | }
35 |
36 | void iReadStream.seek( long offset, eSeekOrigin origin )
37 | {
38 | stream.Seek( offset, (SeekOrigin)(byte)origin );
39 | }
40 |
41 | private bool disposedValue = false; // To detect redundant calls
42 |
43 | protected virtual void Dispose( bool disposing )
44 | {
45 | if( !disposedValue )
46 | {
47 | if( disposing )
48 | stream?.Dispose();
49 | disposedValue = true;
50 | }
51 | }
52 |
53 | public void Dispose()
54 | {
55 | Dispose( true );
56 | }
57 | void iComDisposable.lastNativeReferenceReleased()
58 | {
59 | Dispose( true );
60 | }
61 |
62 | void iReadStream.getPosition( out long length )
63 | {
64 | length = stream.Position;
65 | }
66 |
67 | static ManagedWrapperCache.Entry factory( Stream managed, bool addRef )
68 | {
69 | NativeReadStream wrapper = new NativeReadStream( managed );
70 | IntPtr native = ManagedWrapper.wrap( wrapper, addRef );
71 | return new ManagedWrapperCache.Entry( native, wrapper );
72 | }
73 | static readonly ManagedWrapperCache cache = new ManagedWrapperCache( factory );
74 |
75 | public static IntPtr create( Stream managed, bool addRef )
76 | {
77 | return cache.wrap( managed, addRef );
78 | }
79 | }
80 | }
--------------------------------------------------------------------------------
/ComLight/IO/NativeWriteStream.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Runtime.CompilerServices;
4 | using System.Runtime.InteropServices;
5 |
6 | namespace ComLight.IO
7 | {
8 | /// Wraps .NET stream into native iWriteStream
9 | class NativeWriteStream: iWriteStream, iComDisposable
10 | {
11 | readonly Stream stream;
12 |
13 | NativeWriteStream( Stream stream )
14 | {
15 | this.stream = stream;
16 | }
17 |
18 | void iWriteStream.flush()
19 | {
20 | stream.Flush();
21 | }
22 |
23 | void iComDisposable.lastNativeReferenceReleased()
24 | {
25 | stream?.Dispose();
26 | }
27 |
28 | #if !NETCOREAPP
29 | unsafe
30 | #endif
31 | void iWriteStream.write( ref byte lpBuffer, int nNumberOfBytesToWrite )
32 | {
33 | #if NETCOREAPP
34 | var span = MemoryMarshal.CreateReadOnlySpan( ref lpBuffer, nNumberOfBytesToWrite );
35 | #else
36 | var span = new ReadOnlySpan( Unsafe.AsPointer( ref lpBuffer ), nNumberOfBytesToWrite );
37 | #endif
38 | stream.Write( span );
39 | }
40 |
41 | static ManagedWrapperCache.Entry factory( Stream managed, bool addRef )
42 | {
43 | NativeWriteStream wrapper = new NativeWriteStream( managed );
44 | IntPtr native = ManagedWrapper.wrap( wrapper, addRef );
45 | return new ManagedWrapperCache.Entry( native, wrapper );
46 | }
47 | static readonly ManagedWrapperCache cache = new ManagedWrapperCache( factory );
48 |
49 | public static IntPtr create( Stream managed, bool addRef )
50 | {
51 | return cache.wrap( managed, addRef );
52 | }
53 | }
54 | }
--------------------------------------------------------------------------------
/ComLight/IO/ReadStreamAttribute.cs:
--------------------------------------------------------------------------------
1 | using ComLight.IO;
2 | using System;
3 |
4 | namespace ComLight
5 | {
6 | /// Apply on parameter of type to marshal into iReadStream native interface, allowing to read from streams implemented on the other side of the interop.
7 | [AttributeUsage( AttributeTargets.Parameter )]
8 | public class ReadStreamAttribute: MarshallerAttribute
9 | {
10 | ///
11 | public ReadStreamAttribute() :
12 | base( typeof( ReadStreamMarshal ) )
13 | { }
14 | }
15 | }
--------------------------------------------------------------------------------
/ComLight/IO/ReadStreamMarshal.cs:
--------------------------------------------------------------------------------
1 | using ComLight.Marshalling;
2 | using System;
3 | using System.IO;
4 | using System.Linq.Expressions;
5 | using System.Reflection;
6 |
7 | namespace ComLight.IO
8 | {
9 | class ReadStreamMarshal: iCustomMarshal
10 | {
11 | public override Type getNativeType( ParameterInfo managedParameter )
12 | {
13 | Type managed = managedParameter.ParameterType;
14 | if( managed == typeof( Stream ) )
15 | return typeof( IntPtr );
16 | if( managed == typeof( Stream ).MakeByRefType() )
17 | {
18 | if( managedParameter.IsIn )
19 | throw new ArgumentException( "[ReadStream] doesn't support ref parameters" );
20 | return MiscUtils.intPtrRef;
21 | }
22 | throw new ArgumentException( "[ReadStream] must be applied to a parameter of type Stream" );
23 | }
24 |
25 | static readonly MethodInfo miWrapManaged;
26 | static readonly MethodInfo miWrapNative;
27 |
28 | static ReadStreamMarshal()
29 | {
30 | BindingFlags bf = BindingFlags.Public | BindingFlags.Static;
31 | miWrapManaged = typeof( NativeReadStream ).GetMethod( "create", bf );
32 | miWrapNative = typeof( ManagedReadStream ).GetMethod( "create", bf );
33 | }
34 |
35 | public override Expressions native( ParameterExpression eManaged, bool isInput )
36 | {
37 | if( isInput )
38 | return Expressions.input( Expression.Call( miWrapManaged, eManaged, MiscUtils.eFalse ), eManaged );
39 |
40 | var eNative = Expression.Variable( typeof( IntPtr ) );
41 | var eWrap = Expression.Call( miWrapNative, eNative );
42 | var eResult = Expression.Assign( eManaged, eWrap );
43 | return Expressions.output( eNative, eResult );
44 | }
45 |
46 | public override Expressions managed( ParameterExpression eNative, bool isInput )
47 | {
48 | if( isInput )
49 | return Expressions.input( Expression.Call( miWrapNative, eNative ), null );
50 |
51 | var eManaged = Expression.Variable( typeof( Stream ) );
52 | var eWrap = Expression.Call( miWrapManaged, eManaged, MiscUtils.eTrue );
53 | var eResult = Expression.Assign( eNative, eWrap );
54 | return Expressions.output( eManaged, eResult );
55 | }
56 | }
57 | }
--------------------------------------------------------------------------------
/ComLight/IO/WriteStreamAttribute.cs:
--------------------------------------------------------------------------------
1 | using ComLight.IO;
2 | using System;
3 |
4 | namespace ComLight
5 | {
6 | /// Apply on parameter of type to marshal into iWriteStream native interface, allowing to write streams implemented on the other side of the interop.
7 | [AttributeUsage( AttributeTargets.Parameter )]
8 | public class WriteStreamAttribute: MarshallerAttribute
9 | {
10 | ///
11 | public WriteStreamAttribute() :
12 | base( typeof( WriteStreamMarshal ) )
13 | { }
14 | }
15 | }
--------------------------------------------------------------------------------
/ComLight/IO/WriteStreamMarshal.cs:
--------------------------------------------------------------------------------
1 | using ComLight.Marshalling;
2 | using System;
3 | using System.IO;
4 | using System.Linq.Expressions;
5 | using System.Reflection;
6 |
7 | namespace ComLight.IO
8 | {
9 | class WriteStreamMarshal: iCustomMarshal
10 | {
11 | public override Type getNativeType( ParameterInfo managedParameter )
12 | {
13 | Type managed = managedParameter.ParameterType;
14 | if( managed == typeof( Stream ) )
15 | return typeof( IntPtr );
16 | if( managed == typeof( Stream ).MakeByRefType() )
17 | {
18 | if( managedParameter.IsIn )
19 | throw new ArgumentException( "[WriteStream] doesn't support ref parameters" );
20 | return MiscUtils.intPtrRef;
21 | }
22 | throw new ArgumentException( "[WriteStream] must be applied to a parameter of type Stream" );
23 | }
24 |
25 | static readonly MethodInfo miWrapManaged;
26 | static readonly MethodInfo miWrapNative;
27 |
28 | static WriteStreamMarshal()
29 | {
30 | BindingFlags bf = BindingFlags.Public | BindingFlags.Static;
31 | miWrapManaged = typeof( NativeWriteStream ).GetMethod( "create", bf );
32 | miWrapNative = typeof( ManagedWriteStream ).GetMethod( "create", bf );
33 | }
34 |
35 | public override Expressions native( ParameterExpression eManaged, bool isInput )
36 | {
37 | if( isInput )
38 | return Expressions.input( Expression.Call( miWrapManaged, eManaged, MiscUtils.eFalse ), eManaged );
39 |
40 | var eNative = Expression.Variable( typeof( IntPtr ) );
41 | var eWrap = Expression.Call( miWrapNative, eNative );
42 | var eResult = Expression.Assign( eManaged, eWrap );
43 | return Expressions.output( eNative, eResult );
44 | }
45 |
46 | public override Expressions managed( ParameterExpression eNative, bool isInput )
47 | {
48 | if( isInput )
49 | return Expressions.input( Expression.Call( miWrapNative, eNative ), null );
50 |
51 | var eManaged = Expression.Variable( typeof( Stream ) );
52 | var eWrap = Expression.Call( miWrapManaged, eManaged, MiscUtils.eTrue );
53 | var eResult = Expression.Assign( eNative, eWrap );
54 | return Expressions.output( eManaged, eResult );
55 | }
56 | }
57 | }
--------------------------------------------------------------------------------
/ComLight/IO/iReadStream.cs:
--------------------------------------------------------------------------------
1 | namespace ComLight.IO
2 | {
3 | /// Specifies the position in a stream to use for seeking.
4 | public enum eSeekOrigin: byte
5 | {
6 | /// Specifies the beginning of a stream.
7 | Begin = 0,
8 | /// Specifies the current position within a stream.
9 | Current = 1,
10 | /// Specifies the end of a stream.
11 | End = 2
12 | }
13 |
14 | /// Readonly byte stream interface.
15 | [ComInterface( "006af6db-734e-4595-8c94-19304b2389ac" )]
16 | public interface iReadStream
17 | {
18 | /// Read a sequence of bytes from the current stream and advances the position within the stream by the number of bytes read.
19 | void read( ref byte lpBuffer, int nNumberOfBytesToRead, out int lpNumberOfBytesRead );
20 | /// Set the position within the current stream.
21 | void seek( long offset, eSeekOrigin origin );
22 | /// Get the position within the current stream.
23 | void getPosition( out long length );
24 | /// Get the length in bytes of the stream.
25 | void getLength( out long length );
26 | }
27 | }
--------------------------------------------------------------------------------
/ComLight/IO/iWriteStream.cs:
--------------------------------------------------------------------------------
1 | using System.Runtime.InteropServices;
2 |
3 | namespace ComLight
4 | {
5 | /// Write only byte stream interface.
6 | [ComInterface( "d7c3eb39-9170-43b9-ba98-2ea1f2fed8a8" )]
7 | public interface iWriteStream
8 | {
9 | /// write a sequence of bytes to the current stream and advance the current position within this stream by the number of bytes written.
10 | void write( [In] ref byte lpBuffer, int nNumberOfBytesToWrite );
11 | /// Clear all buffers for this stream and causes any buffered data to be written to the underlying device.
12 | void flush();
13 | }
14 | }
--------------------------------------------------------------------------------
/ComLight/IUnknown.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Runtime.InteropServices;
3 |
4 | namespace ComLight
5 | {
6 | /// Native IUnknown stuff.
7 | static class IUnknown
8 | {
9 | [UnmanagedFunctionPointer( RuntimeClass.defaultCallingConvention )]
10 | public delegate int QueryInterface( IntPtr pThis, [In] ref Guid iid, out IntPtr result );
11 |
12 | [UnmanagedFunctionPointer( RuntimeClass.defaultCallingConvention )]
13 | public delegate uint AddRef( IntPtr pThis );
14 |
15 | [UnmanagedFunctionPointer( RuntimeClass.defaultCallingConvention )]
16 | public delegate uint Release( IntPtr pThis );
17 |
18 | public static readonly Guid iid = new Guid( "00000000-0000-0000-c000-000000000046" );
19 |
20 | public const int S_OK = 0;
21 | public const int S_FALSE = 1;
22 | public const int E_NOINTERFACE = unchecked((int)0x80004002L);
23 | public const int E_UNEXPECTED = unchecked((int)0x8000FFFFL);
24 | }
25 | }
--------------------------------------------------------------------------------
/ComLight/ManagedObject.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Diagnostics;
3 | using System.Runtime.InteropServices;
4 | using System.Threading;
5 |
6 | namespace ComLight
7 | {
8 | /// Implements a COM interface around managed object.
9 | /// This class implements an equivalent of COM Callable Wrapper
10 | sealed class ManagedObject
11 | {
12 | /// COM interface pointer, just good enough for C++ to call the methods.
13 | public IntPtr address => gchNativeData.AddrOfPinnedObject();
14 |
15 | /// The managed object implementing that interface
16 | public readonly object managed;
17 | /// Pinned vtable data, plus one extra entry at the start.
18 | readonly GCHandle gchNativeData;
19 | /// If C++ code calls AddRef on the COM pointer, will use this GCHandle to protect the C# object from garbage collector.
20 | GCHandle gchManagedObject;
21 | /// Reference counter, it only counts references from C++ code.
22 | volatile int nativeRefCounter = 0;
23 |
24 | /// IUnknown function pointers
25 | readonly IUnknown.QueryInterface queryInterface;
26 | readonly IUnknown.AddRef addRef;
27 | readonly IUnknown.Release release;
28 |
29 | readonly Guid iid;
30 | readonly Delegate[] delegates;
31 |
32 | public ManagedObject( object managed, Guid iid, Delegate[] delegates )
33 | {
34 | this.managed = managed;
35 | this.iid = iid;
36 |
37 | IntPtr[] nativeTable = new IntPtr[ delegates.Length + 4 ];
38 | gchNativeData = GCHandle.Alloc( nativeTable, GCHandleType.Pinned );
39 | Cache.Managed.add( address, this );
40 |
41 | // A COM pointer is an address of address: "this" points to vtable pointer, vtable pointer points to the first vtable entry, the rest of the entries follow.
42 | // We want binary compatibility, so nativeTable[ 0 ] contains address of nativeTable[ 1 ], and methods function pointers start at nativeTable[ 1 ].
43 | nativeTable[ 0 ] = address + Marshal.SizeOf();
44 |
45 | // Build 3 first entries of the vtable, with IUnknown methods
46 | queryInterface = delegate ( IntPtr pThis, ref Guid ii, out IntPtr result ) { Debug.Assert( pThis == address ); return implQueryInterface( ref ii, out result ); };
47 | nativeTable[ 1 ] = Marshal.GetFunctionPointerForDelegate( queryInterface );
48 |
49 | addRef = delegate ( IntPtr pThis ) { Debug.Assert( pThis == address ); return implAddRef(); };
50 | nativeTable[ 2 ] = Marshal.GetFunctionPointerForDelegate( addRef );
51 |
52 | release = delegate ( IntPtr pThis ) { Debug.Assert( pThis == address ); return implRelease(); };
53 | nativeTable[ 3 ] = Marshal.GetFunctionPointerForDelegate( release );
54 |
55 | // Custom methods entries of the vtable
56 | for( int i = 0; i < delegates.Length; i++ )
57 | nativeTable[ i + 4 ] = Marshal.GetFunctionPointerForDelegate( delegates[ i ] );
58 |
59 | // Retain C# delegates for custom methods in the field of this class.
60 | // Failing to do so causes a runtime crash "A callback was made on a garbage collected delegate of type ComLight.Wrappers!…"
61 | this.delegates = delegates;
62 | }
63 |
64 | int implQueryInterface( [In] ref Guid ii, out IntPtr result )
65 | {
66 | if( ii == iid || ii == IUnknown.iid )
67 | {
68 | // From native code point of view, this COM object only supports 2 COM interfaces: IUnknown, and the one with the IID that was passed to the constructor.
69 | // In both cases, besides just returning the native pointer, we need to increment the ref.counter.
70 | result = address;
71 | implAddRef();
72 | return IUnknown.S_OK;
73 | }
74 | result = IntPtr.Zero;
75 | return IUnknown.E_NOINTERFACE;
76 | }
77 |
78 | uint implAddRef()
79 | {
80 | int res = Interlocked.Increment( ref nativeRefCounter );
81 | if( 1 == res )
82 | {
83 | Debug.Assert( !gchManagedObject.IsAllocated );
84 | // Retain the original user-provided object.
85 | // This ManagedObject wrapper is retained too, because ManagedWrapper.WrappersCache has a ConditionalWeakTable for that.
86 | gchManagedObject = GCHandle.Alloc( managed );
87 | }
88 | return (uint)res;
89 | }
90 |
91 | uint implRelease()
92 | {
93 | int res = Interlocked.Decrement( ref nativeRefCounter );
94 | Debug.Assert( res >= 0 );
95 | if( 0 == res )
96 | {
97 | Debug.Assert( gchManagedObject.IsAllocated );
98 |
99 | // This C#-implemented COM objects was retained and then released by C++ code.
100 | if( managed is iComDisposable cd )
101 | {
102 | try
103 | {
104 | cd.lastNativeReferenceReleased();
105 | }
106 | finally
107 | {
108 | gchManagedObject.Free();
109 | }
110 | }
111 | else
112 | gchManagedObject.Free();
113 | }
114 | return (uint)res;
115 | }
116 |
117 | ~ManagedObject()
118 | {
119 | Debug.Assert( 0 == nativeRefCounter );
120 | Debug.Assert( !gchManagedObject.IsAllocated );
121 |
122 | if( gchManagedObject.IsAllocated )
123 | gchManagedObject.Free();
124 |
125 | if( gchNativeData.IsAllocated )
126 | {
127 | Cache.Managed.drop( address );
128 | gchNativeData.Free();
129 | }
130 | }
131 |
132 | internal void callAddRef()
133 | {
134 | implAddRef();
135 | }
136 | };
137 | }
--------------------------------------------------------------------------------
/ComLight/ManagedWrapper.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Diagnostics;
4 | using System.Reflection;
5 | using System.Runtime.CompilerServices;
6 |
7 | namespace ComLight
8 | {
9 | /// Wraps managed interfaces into COM objects callable by native code.
10 | public static partial class ManagedWrapper
11 | {
12 | /// When native code doesn't bother calling AddRef on these interfaces, the lifetime of the wrappers is linked to the lifetime of the interface objects. This class implements that link.
13 | static class WrappersCache where I : class
14 | {
15 | static readonly ConditionalWeakTable table = new ConditionalWeakTable();
16 |
17 | public static void add( I obj, ManagedObject wrapper )
18 | {
19 | table.Add( obj, wrapper );
20 | }
21 |
22 | public static IntPtr? lookup( I obj )
23 | {
24 | ManagedObject result;
25 | if( !table.TryGetValue( obj, out result ) )
26 | return null;
27 | return result.address;
28 | }
29 | }
30 |
31 | static readonly object syncRoot = new object();
32 | static readonly Dictionary> cache = new Dictionary>();
33 |
34 | /// Create a factory which supports two-way marshaling.
35 | static Func