├── .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 createTwoWayFactory( Guid iid ) where I : class 36 | { 37 | // The builder gets captured by the lambda. 38 | // This is what we want, the constructor takes noticeable time, the code outside the lambda runs once per interface type, the code inside lambda runs once per object instance. 39 | InterfaceBuilder builder = new InterfaceBuilder( typeof( I ) ); 40 | 41 | return ( object obj, bool addRef ) => 42 | { 43 | if( null == obj ) 44 | { 45 | // Marshalling null 46 | return IntPtr.Zero; 47 | } 48 | Debug.Assert( obj is I ); 49 | 50 | if( obj is RuntimeClass rc ) 51 | { 52 | // That .NET object is not actually managed, it's a wrapper around C++ implemented COM interface. 53 | if( rc.iid == iid ) 54 | { 55 | // It wraps around the same interface 56 | if( addRef ) 57 | rc.addRef(); 58 | return rc.nativePointer; 59 | } 60 | 61 | // It wraps around different interface. Call QueryInterface on the native object. 62 | return rc.queryInterface( iid, addRef ); 63 | } 64 | 65 | // It could be the same managed object is reused across native calls. If that's the case, the cache already contains the native pointer. 66 | I managed = (I)obj; 67 | IntPtr? wrapped = WrappersCache.lookup( managed ); 68 | if( wrapped.HasValue ) 69 | return wrapped.Value; 70 | 71 | Delegate[] delegates = builder.compile( managed ); 72 | ManagedObject wrapper = new ManagedObject( managed, iid, delegates ); 73 | WrappersCache.add( managed, wrapper ); 74 | if( addRef ) 75 | wrapper.callAddRef(); 76 | return wrapper.address; 77 | }; 78 | } 79 | 80 | /// Create a factory which only supports objects implemented in .NET 81 | static Func createOneWayToNativeFactory( Guid iid ) where I : class 82 | { 83 | // The builder gets captured by the lambda. 84 | // This is what we want, the constructor takes noticeable time, the code outside the lambda runs once per interface type, the code inside lambda runs once per object instance. 85 | InterfaceBuilder builder = new InterfaceBuilder( typeof( I ) ); 86 | 87 | return ( object obj, bool addRef ) => 88 | { 89 | if( null == obj ) 90 | { 91 | // Marshalling null 92 | return IntPtr.Zero; 93 | } 94 | Debug.Assert( obj is I ); 95 | 96 | // It could be the same managed object is reused across native calls. If that's the case, the cache already contains the native pointer. 97 | I managed = (I)obj; 98 | IntPtr? wrapped = WrappersCache.lookup( managed ); 99 | if( wrapped.HasValue ) 100 | return wrapped.Value; 101 | 102 | Delegate[] delegates = builder.compile( managed ); 103 | ManagedObject wrapper = new ManagedObject( managed, iid, delegates ); 104 | WrappersCache.add( managed, wrapper ); 105 | if( addRef ) 106 | wrapper.callAddRef(); 107 | return wrapper.address; 108 | }; 109 | } 110 | 111 | /// Create a factory which only supports objects implemented in C++ 112 | static Func createOneWayToManagedFactory( Type tInterface, Guid iid ) 113 | { 114 | string directionNotSupportedError = $"The COM interface { tInterface.FullName } doesn't support managed to native marshaling direction"; 115 | 116 | return ( object obj, bool addRef ) => 117 | { 118 | if( null == obj ) 119 | { 120 | // Marshalling null 121 | return IntPtr.Zero; 122 | } 123 | 124 | if( obj is RuntimeClass rc ) 125 | { 126 | // That .NET object is not actually managed, it's a wrapper around C++ implemented COM interface. We can marshal these just fine. 127 | if( rc.iid == iid ) 128 | { 129 | // It wraps around the same interface 130 | if( addRef ) 131 | rc.addRef(); 132 | return rc.nativePointer; 133 | } 134 | 135 | // It wraps around different interface. Call QueryInterface on the native object. 136 | return rc.queryInterface( iid, addRef ); 137 | } 138 | 139 | throw new NotSupportedException( directionNotSupportedError ); 140 | }; 141 | } 142 | 143 | static Func getFactory() where I : class 144 | { 145 | Type tInterface = typeof( I ); 146 | Func result; 147 | lock( syncRoot ) 148 | { 149 | if( cache.TryGetValue( tInterface, out result ) ) 150 | return result; 151 | 152 | Guid iid = ReflectionUtils.checkInterface( tInterface ); 153 | 154 | var attr = tInterface.GetCustomAttribute(); 155 | if( null == attr ) 156 | throw new ArgumentException( $"The type { tInterface.FullName } doesn't have [ComInterface] applied." ); 157 | 158 | switch( attr.marshalDirection ) 159 | { 160 | case eMarshalDirection.BothWays: 161 | result = createTwoWayFactory( iid ); 162 | break; 163 | case eMarshalDirection.ToManaged: 164 | result = createOneWayToManagedFactory( tInterface, iid ); 165 | break; 166 | case eMarshalDirection.ToNative: 167 | result = createOneWayToNativeFactory( iid ); 168 | break; 169 | default: 170 | throw new ArgumentException( $"Unexpected eMarshalDirection value { (byte)attr.marshalDirection }" ); 171 | } 172 | 173 | cache.Add( tInterface, result ); 174 | return result; 175 | } 176 | } 177 | 178 | /// Wrap a C# interface into a COM object callable from native code 179 | /// COM interface type 180 | /// COM interface instance 181 | /// Pass True to increment native ref.counter, do that when you want to move ownership to C++ code. 182 | /// Native COM pointer of the wrapper 183 | public static IntPtr wrap( object obj, bool addRef ) where I : class 184 | { 185 | Func factory = getFactory(); 186 | return factory( obj, addRef ); 187 | } 188 | } 189 | } -------------------------------------------------------------------------------- /ComLight/Marshaler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | 4 | namespace ComLight 5 | { 6 | /// Marshaller which wraps native COM pointer into callable wrapper, or constructs C++ vtables around .NET objects 7 | /// COM interface, must be marked with . 8 | public class Marshaler: ICustomMarshaler 9 | where I : class 10 | { 11 | readonly Guid iid; 12 | /// 13 | public Marshaler() 14 | { 15 | iid = ReflectionUtils.checkInterface( typeof( I ) ); 16 | } 17 | 18 | void ICustomMarshaler.CleanUpManagedData( object ManagedObj ) 19 | { 20 | } 21 | 22 | void ICustomMarshaler.CleanUpNativeData( IntPtr pNativeData ) 23 | { 24 | } 25 | 26 | int ICustomMarshaler.GetNativeDataSize() 27 | { 28 | return Marshal.SizeOf(); 29 | } 30 | 31 | IntPtr ICustomMarshaler.MarshalManagedToNative( object ManagedObj ) 32 | { 33 | // Build these vtables on top of the managed interface. 34 | return ManagedWrapper.wrap( ManagedObj, false ); 35 | } 36 | 37 | object ICustomMarshaler.MarshalNativeToManaged( IntPtr pNativeData ) 38 | { 39 | if( pNativeData == IntPtr.Zero ) 40 | return null; 41 | return NativeWrapper.wrap( typeof( I ), pNativeData ); 42 | } 43 | 44 | static readonly ICustomMarshaler instance = new Marshaler(); 45 | 46 | /// In addition to implementing the ICustomMarshaler interface, custom marshalers must implement a static method called GetInstance that accepts a String as a parameter and has a return type of ICustomMarshaler. 47 | public static ICustomMarshaler GetInstance( string pstrCookie ) 48 | { 49 | return instance; 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /ComLight/Marshalling/Expressions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq.Expressions; 3 | using System.Reflection; 4 | 5 | namespace ComLight.Marshalling 6 | { 7 | /// Custom marshaling expressions. They are compiled into IL code while creating instances of proxies. 8 | public class Expressions 9 | { 10 | /// Local variables, can be null 11 | public readonly ParameterExpression variable = null; 12 | 13 | /// Expression to pass to the native function 14 | public readonly Expression argument; 15 | 16 | /// Called after the native function completes successfully. Can be null. 17 | public readonly Expression after = null; 18 | 19 | /// Construct with just the argument 20 | Expressions( Expression arg ) 21 | { 22 | argument = arg; 23 | } 24 | 25 | /// Construct with local variables, argument expression, and post expression. 26 | public Expressions( ParameterExpression var1, Expression argument, Expression after ) 27 | { 28 | variable = var1; 29 | this.argument = argument; 30 | this.after = after; 31 | } 32 | 33 | static readonly MethodInfo miKeepAlive = typeof( GC ) 34 | .GetMethod( nameof( GC.KeepAlive ), BindingFlags.Static | BindingFlags.Public ); 35 | 36 | /// Simple marshaling expression for `in` direction 37 | /// Since version 1.3.9, this protects input object from GC for the duration of the C++ call. 38 | public static Expressions input( Expression arg, Expression keepAlive ) 39 | { 40 | if( null != keepAlive ) 41 | { 42 | Expression after = Expression.Call( miKeepAlive, keepAlive ); 43 | return new Expressions( null, arg, after ); 44 | } 45 | else 46 | return new Expressions( arg ); 47 | } 48 | 49 | /// Marshalling expression for `out` direction: declare a local variable, pass it to the delegate, then assign the result output parameter by wrapping the local variable using a custom expression. 50 | public static Expressions output( ParameterExpression var, Expression after ) 51 | { 52 | return new Expressions( var, var, after ); 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /ComLight/Marshalling/InterfaceArrayMarshaller.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq.Expressions; 3 | using System.Reflection; 4 | using System.Reflection.Emit; 5 | using System.Runtime.InteropServices; 6 | 7 | namespace ComLight.Marshalling 8 | { 9 | class InterfaceArrayMarshaller: iCustomMarshal where I : class 10 | { 11 | public override Type getNativeType( ParameterInfo managedParameter ) 12 | { 13 | if( managedParameter.IsOut ) 14 | throw new ApplicationException( @"InterfaceArrayMarshaller can only marshal them one way" ); 15 | Type managed = managedParameter.ParameterType; 16 | if( managed == typeof( I[] ) ) 17 | return typeof( IntPtr[] ); 18 | throw new ApplicationException( @"InterfaceArrayMarshaller is used with a wrong parameter type" ); 19 | } 20 | 21 | public override void applyDelegateParams( ParameterInfo source, ParameterBuilder destination ) 22 | { 23 | destination.applyMarshalAsAttribute( UnmanagedType.LPArray ); 24 | destination.applyInAttribute(); 25 | } 26 | 27 | static IntPtr[] wrapManaged( I[] managed, bool callAddRef ) 28 | { 29 | if( null != managed ) 30 | { 31 | // Not sure about the performance. 32 | // Theoretically, these small arrays should never leave generation 0 of the managed heap. 33 | // Practically, using `new` with GPU calls can create measurable load on GC. 34 | // Maybe we need to do something more sophisticated here, like use ArrayPool.Shared for these arrays. 35 | IntPtr[] result = new IntPtr[ managed.Length ]; 36 | for( int i = 0; i < managed.Length; i++ ) 37 | { 38 | I obj = managed[ i ]; 39 | if( null != obj ) 40 | result[ i ] = ManagedWrapper.wrap( obj, callAddRef ); 41 | } 42 | return result; 43 | } 44 | return null; 45 | } 46 | 47 | /// 48 | static readonly MethodInfo miWrapManaged = typeof( InterfaceArrayMarshaller ) 49 | .GetMethod( "wrapManaged", BindingFlags.Static | BindingFlags.NonPublic ); 50 | 51 | public override Expressions native( ParameterExpression eManaged, bool isInput ) 52 | { 53 | if( isInput ) 54 | return Expressions.input( Expression.Call( miWrapManaged, eManaged, MiscUtils.eFalse ), eManaged ); 55 | 56 | throw new NotSupportedException( "COM interfaces array marshaller doesn't support output parameters" ); 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /ComLight/Marshalling/InterfaceMarshaller.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq.Expressions; 3 | using System.Reflection; 4 | using System.Reflection.Emit; 5 | 6 | namespace ComLight.Marshalling 7 | { 8 | /// Used automatically by the library, when COM objects create or consume other COM objects. 9 | /// In some rare cases you might need to use it directly, . 10 | public class InterfaceMarshaller: iCustomMarshal where I : class 11 | { 12 | /// IntPtr for input parameters, or `ref IntPtr` for output parameters 13 | public override Type getNativeType( ParameterInfo managedParameter ) 14 | { 15 | Type managed = managedParameter.ParameterType; 16 | if( managed == typeof( I ) ) 17 | return typeof( IntPtr ); 18 | if( managed == typeof( I ).MakeByRefType() ) 19 | { 20 | if( managedParameter.IsIn ) 21 | throw new ArgumentException( "COM interfaces can only be marshaled in or out, ref is not supported for them" ); 22 | return MiscUtils.intPtrRef; 23 | } 24 | throw new ApplicationException( $"InterfaceMarshaller is used with a wrong parameter type { managed.FullName }" ); 25 | } 26 | 27 | /// Add [Out] attribute if needed 28 | public override void applyDelegateParams( ParameterInfo source, ParameterBuilder destination ) 29 | { 30 | if( source.IsOut ) 31 | destination.applyOutAttribute(); 32 | } 33 | 34 | /// 35 | static readonly MethodInfo miWrapManaged = typeof( ManagedWrapper ) 36 | .GetMethod( "wrap" ) 37 | .MakeGenericMethod( typeof( I ) ); 38 | 39 | /// 40 | static readonly MethodInfo miWrapNative = typeof( NativeWrapper ) 41 | .GetMethod( "wrap", new Type[ 1 ] { typeof( IntPtr ) } ) 42 | .MakeGenericMethod( typeof( I ) ); 43 | 44 | /// Expressions to convert native COM pointer into .NET object, for marshaling direction. 45 | /// For output parameters it does the opposite, wraps .NET object into new and calls AddRef. 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( I ) ); 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 | /// Expressions to convert .NET object to native COM pointer, for marshaling direction. 58 | /// For output parameters it does the opposite, wraps native pointer into new object. 59 | /// No AddRef is necessary. By convention, C++ COM methods which create or return objects already do that before they return them. 60 | public override Expressions native( ParameterExpression eManaged, bool isInput ) 61 | { 62 | if( isInput ) 63 | return Expressions.input( Expression.Call( miWrapManaged, eManaged, MiscUtils.eFalse ), eManaged ); 64 | 65 | var eNative = Expression.Variable( typeof( IntPtr ) ); 66 | var eWrap = Expression.Call( miWrapNative, eNative ); 67 | var eResult = Expression.Assign( eManaged, eWrap ); 68 | return Expressions.output( eNative, eResult ); 69 | } 70 | } 71 | } -------------------------------------------------------------------------------- /ComLight/Marshalling/MarshallerAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ComLight 4 | { 5 | /// Apply to parameters to implement custom marshaling. 6 | [AttributeUsage( AttributeTargets.Parameter )] 7 | public class MarshallerAttribute: Attribute 8 | { 9 | /// The type that implements 10 | public readonly Type tMarshaller; 11 | 12 | /// Construct with marshaller type, must implement 13 | public MarshallerAttribute( Type t ) 14 | { 15 | if( !typeof( iCustomMarshal ).IsAssignableFrom( t ) ) 16 | throw new ArgumentException( $"Marshaller type { t.FullName } must derive from iCustomMarshal abstract class." ); 17 | tMarshaller = t; 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /ComLight/Marshalling/Marshallers.cs: -------------------------------------------------------------------------------- 1 | using ComLight.Marshalling; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Reflection; 5 | 6 | namespace ComLight 7 | { 8 | static class Marshallers 9 | { 10 | // iCustomMarshal instances aren't supposed to have any state. For better performance, caching them on the hash map. 11 | static readonly object syncRoot = new object(); 12 | static readonly Dictionary cache = new Dictionary(); 13 | 14 | static iCustomMarshal getMarshaller( Type tp ) 15 | { 16 | lock( syncRoot ) 17 | { 18 | iCustomMarshal result; 19 | if( cache.TryGetValue( tp, out result ) ) 20 | return result; 21 | result = (iCustomMarshal)Activator.CreateInstance( tp ); 22 | cache.Add( tp, result ); 23 | return result; 24 | } 25 | } 26 | 27 | /// If the parameter type is an array of COM interfaces, returns type of that interface; otherwise returns null. 28 | static Type interfaceArrayElementType( this Type tParameter ) 29 | { 30 | if( !tParameter.IsArray ) 31 | return null; 32 | Type tElement = tParameter.GetElementType(); 33 | if( !tElement.hasCustomAttribute() ) 34 | return null; 35 | 36 | if( 1 != tParameter.GetArrayRank() ) 37 | throw new ApplicationException( "Trying to marshal multi-dimensional array of COM objects. ComLight runtime doesn't support that." ); 38 | return tElement; 39 | } 40 | 41 | /// If the parameter type is a COM interface, return instance of InterfaceMarshaller<I>. 42 | /// If the parameter has [Marshaller] attribute, return that one. 43 | /// If the parameter is an array of COM interfaces, return instance of InterfaceArrayMarshaller<I>. 44 | /// Otherwise return null. 45 | public static iCustomMarshal customMarshaller( this ParameterInfo pi ) 46 | { 47 | Type tp = pi.ParameterType.unwrapRef(); 48 | 49 | if( tp.hasCustomAttribute() ) 50 | { 51 | var im = typeof( InterfaceMarshaller<> ); 52 | im = im.MakeGenericType( tp ); 53 | return getMarshaller( im ); 54 | } 55 | 56 | if( tp.interfaceArrayElementType() is Type tElement ) 57 | { 58 | var iam = typeof( InterfaceArrayMarshaller<> ); 59 | iam = iam.MakeGenericType( tElement ); 60 | return getMarshaller( iam ); 61 | } 62 | 63 | if( pi.GetCustomAttribute() is MarshallerAttribute a ) 64 | return getMarshaller( a.tMarshaller ); 65 | 66 | return null; 67 | } 68 | 69 | public static bool hasCustomMarshaller( this ParameterInfo pi ) 70 | { 71 | Type tp = pi.ParameterType.unwrapRef(); 72 | if( tp.hasCustomAttribute() ) 73 | return true; 74 | if( pi.hasCustomAttribute() ) 75 | return true; 76 | // COM interface arrays don't have any special attributes applied in the C# code of the source interface, yet they need custom marshaling as well. 77 | if( null != tp.interfaceArrayElementType() ) 78 | return true; 79 | return false; 80 | } 81 | } 82 | } -------------------------------------------------------------------------------- /ComLight/Marshalling/iCustomMarshal.cs: -------------------------------------------------------------------------------- 1 | using ComLight.Marshalling; 2 | using System; 3 | using System.Linq.Expressions; 4 | using System.Reflection; 5 | using System.Reflection.Emit; 6 | 7 | namespace ComLight 8 | { 9 | /// A custom marshaller. 10 | /// If you're implementing this class, note that for performance reasons the library only creates a single instance of each type of marshaller, regardless on how many methods or interfaces are using it. 11 | public abstract class iCustomMarshal 12 | { 13 | /// Convert parameter type, the input is what's in C# interface, the output is what C++ will get. 14 | public abstract Type getNativeType( ParameterInfo managedParameter ); 15 | 16 | /// Apply optional attributes to native delegate parameter. 17 | public virtual void applyDelegateParams( ParameterInfo source, ParameterBuilder destination ) 18 | { } 19 | 20 | /// Expressions to convert .NET types to native types, for marshaling direction. 21 | public virtual Expressions native( ParameterExpression eManaged, bool isInput ) 22 | { 23 | throw new NotSupportedException( $"The marshaller type { GetType().FullName } doesn't support C# to C++ direction" ); 24 | } 25 | 26 | /// Expressions to convert native types to .NET types, for marshaling direction. 27 | public virtual Expressions managed( ParameterExpression eNative, bool isInput ) 28 | { 29 | throw new NotSupportedException( $"The marshaller type { GetType().FullName } doesn't support C++ to C# direction" ); 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /ComLight/NativeStringAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ComLight 4 | { 5 | /// Apply this attribute on string parameters to marshal as null-terminated C string, UTF-16 wchar_t on Windows, UTF-8 char on Linux. 6 | /// This corresponds to LPCTSTR typedef on the native side of the interop. 7 | [AttributeUsage( AttributeTargets.Parameter )] 8 | public class NativeStringAttribute: Attribute 9 | { } 10 | } -------------------------------------------------------------------------------- /ComLight/NativeWrapper.cs: -------------------------------------------------------------------------------- 1 | using ComLight.Emit; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Reflection; 5 | 6 | namespace ComLight 7 | { 8 | /// Wraps C++ COM interfaces into dynamically built callable wrappers, derived from . 9 | public static class NativeWrapper 10 | { 11 | // The factories are relatively expensive to build: reflection, dynamic compilation, other shenanigans. 12 | // Also the name of dynamically built classes only depend on the interface + namespace of it, building 2 factories for the same interface would result in name conflict. 13 | // That's why caching. 14 | static readonly object syncRoot = new object(); 15 | static readonly Dictionary> factories = new Dictionary>(); 16 | 17 | /// Create a factory which supports two-way marshaling. 18 | static Func createTwoWayFactory( Type tInterface ) 19 | { 20 | Func newProxy = Proxy.build( tInterface ); 21 | 22 | return ( IntPtr pNative ) => 23 | { 24 | if( pNative == IntPtr.Zero ) 25 | return null; 26 | 27 | RuntimeClass rc = Cache.Native.lookup( pNative, tInterface ); 28 | if( null != rc ) 29 | return rc; 30 | 31 | ManagedObject mo = Cache.Managed.lookup( pNative ); 32 | if( null != mo ) 33 | return mo.managed; 34 | 35 | return newProxy( pNative ); 36 | }; 37 | } 38 | 39 | /// Create a factory which only supports objects implemented in C++ 40 | static Func createOneWayToManagedFactory( Type tInterface ) 41 | { 42 | Func newProxy = Proxy.build( tInterface ); 43 | 44 | return ( IntPtr pNative ) => 45 | { 46 | if( pNative == IntPtr.Zero ) 47 | return null; 48 | 49 | RuntimeClass rc = Cache.Native.lookup( pNative, tInterface ); 50 | if( null != rc ) 51 | return rc; 52 | 53 | return newProxy( pNative ); 54 | }; 55 | } 56 | 57 | /// Create a factory which only supports objects implemented in .NET. Will throw an exception when given a COM object which is not implemented in C#. 58 | static Func createOneWayToNativeFactory( Type tInterface ) 59 | { 60 | string directionNotSupportedError = $"The COM interface { tInterface.FullName } doesn't support native to managed marshaling direction"; 61 | 62 | return ( IntPtr pNative ) => 63 | { 64 | if( pNative == IntPtr.Zero ) 65 | return null; 66 | ManagedObject mo = Cache.Managed.lookup( pNative ); 67 | if( null != mo ) 68 | return mo.managed; 69 | throw new NotSupportedException( directionNotSupportedError ); 70 | }; 71 | } 72 | 73 | /// Having an interface type, return a factory that wraps C++ com pointer into .NET object. 74 | /// Factories are relatively expensive to create, and are cached in this class. 75 | public static Func getFactory( Type tInterface ) 76 | { 77 | Func factory = null; 78 | lock( syncRoot ) 79 | { 80 | if( factories.TryGetValue( tInterface, out factory ) ) 81 | return factory; 82 | 83 | var attr = tInterface.GetCustomAttribute(); 84 | if( null == attr ) 85 | throw new ArgumentException( $"The type { tInterface.FullName } doesn't have [ComInterface] applied." ); 86 | 87 | switch( attr.marshalDirection ) 88 | { 89 | case eMarshalDirection.BothWays: 90 | factory = createTwoWayFactory( tInterface ); 91 | break; 92 | case eMarshalDirection.ToManaged: 93 | factory = createOneWayToManagedFactory( tInterface ); 94 | break; 95 | case eMarshalDirection.ToNative: 96 | factory = createOneWayToNativeFactory( tInterface ); 97 | break; 98 | default: 99 | throw new ArgumentException( $"Unexpected eMarshalDirection value { (byte)attr.marshalDirection }" ); 100 | } 101 | factories.Add( tInterface, factory ); 102 | return factory; 103 | } 104 | } 105 | 106 | /// Wrap native COM interface pointer into .NET object 107 | /// COM interface .NET type 108 | /// Native COM object pointer 109 | public static object wrap( Type tInterface, IntPtr nativeComPointer ) 110 | { 111 | if( nativeComPointer == IntPtr.Zero ) 112 | return null; // Best case performance-wise, BTW 113 | Func factory = getFactory( tInterface ); 114 | return factory( nativeComPointer ); 115 | } 116 | 117 | /// Wrap native COM interface pointer into .NET object 118 | /// COM interface .NET type 119 | /// Native COM object pointer 120 | public static I wrap( IntPtr nativeComPointer ) where I : class 121 | { 122 | return (I)wrap( typeof( I ), nativeComPointer ); 123 | } 124 | } 125 | } -------------------------------------------------------------------------------- /ComLight/PropertyAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ComLight 4 | { 5 | /// Apply this attribute to properties if you want to customize the mapping between names of property and getter/setter methods 6 | [AttributeUsage( AttributeTargets.Property )] 7 | public class PropertyAttribute: Attribute 8 | { 9 | internal readonly string getterMethod; 10 | internal readonly string setterMethod; 11 | 12 | /// Specify custom name. 13 | public PropertyAttribute( string name ) 14 | { 15 | getterMethod = "get" + name; 16 | setterMethod = "set" + name; 17 | } 18 | 19 | /// Specify custom names for both getter and setter. 20 | public PropertyAttribute( string getter, string setter ) 21 | { 22 | getterMethod = getter; 23 | setterMethod = setter; 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /ComLight/Readme.md: -------------------------------------------------------------------------------- 1 | This package implements a lightweight cross-platform COM interop library for Windows and Linux. 2 | Specifically, it allows to expose C++ objects to .NET, and .NET objects to C++. 3 | The current version targets 3 platforms: .NET framework 4.7.2, .NET 8.0, and VC++. 4 | 5 | The library is designed under the assumption the entry point of your program is a .NET application which needs to use unmanaged C++ code compiled into a native DLL. 6 | 7 | By 2024, I have used the library extensively in multiple projects which target Win32, Win64, also Linux running on AMD64, ARMv7, and ARM64 CPUs. 8 | Some of these Linuxes were Alpine, the C++ side of the interop is very simple, does not even depend on `glibc`. 9 | 10 | The library only uses good part of the COM, which is the `IUnknown` ABI. 11 | It does not implement type libraries. 12 | It’s your responsibility to write both C++ and C# language projections of the COM interfaces you use. The included C++ headers, and C# custom attributes, make it easy to do so. 13 | 14 | The interop is limited to COM objects implemented by DLLs running within the current process. 15 | The library only supports `IUnknown`-based COM interfaces, it doesn’t understand `IDispatch`. 16 | You can only use simple types in your interfaces: primitives, structures, strings, pointers, arrays, function pointers, but not VARIANT or SAFEARRAY. 17 | 18 | It doesn’t prevent you from calling the same COM object by multiple threads concurrently i.e. it doesn’t have a concept of apartments and treats all objects as free threaded. 19 | If you call your objects concurrently, it’s your responsibility to make sure your objects are thread safe and reentrant. 20 | 21 | It does not implement class IDs or type registration. 22 | A class factory for C++ implemented objects is merely a C function which returns `IUnknown`-derived interface in an output parameter. -------------------------------------------------------------------------------- /ComLight/RetValIndexAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ComLight 4 | { 5 | /// Apply this attribute to a method with C++ API HRESULT method( ISomething** result) if you want C# API ISomething method() 6 | [AttributeUsage( AttributeTargets.Method, AllowMultiple = false )] 7 | public class RetValIndexAttribute: Attribute 8 | { 9 | /// Zero-based index of the RetVal argument in C++ projection of the COM interface. 10 | public readonly byte index; 11 | 12 | /// Constructor 13 | public RetValIndexAttribute( byte idx = 0 ) 14 | { 15 | index = idx; 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /ComLight/RuntimeClass.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | 4 | namespace ComLight 5 | { 6 | /// Abstract base class for generated proxies of C++ objects. Consumes IUnknown methods, implements IDisposable and finalizer. 7 | public abstract class RuntimeClass: IDisposable 8 | { 9 | /// For performance reason, runtime-generated derived classes access this field directly, without the overhead of property getter call. 10 | protected IntPtr m_nativePointer = IntPtr.Zero; 11 | /// Native COM pointer 12 | public IntPtr nativePointer => m_nativePointer; 13 | 14 | /// Calling convention for the interface methods. 15 | /// Apparently StdCall is x86 only, on AMD64 something else is used instead. Fortunately, that something else appears to be binary compatible with both GCC and VC++ defaults. 16 | public const CallingConvention defaultCallingConvention = CallingConvention.StdCall; 17 | 18 | readonly IUnknown.QueryInterface QueryInterface; 19 | readonly IUnknown.AddRef AddRef; 20 | readonly IUnknown.Release Release; 21 | 22 | /// Construct the wrapper. 23 | public RuntimeClass( IntPtr ptr, IntPtr[] vtbl, Guid iid ) 24 | { 25 | m_nativePointer = ptr; 26 | this.iid = iid; 27 | QueryInterface = Marshal.GetDelegateForFunctionPointer( vtbl[ 0 ] ); 28 | AddRef = Marshal.GetDelegateForFunctionPointer( vtbl[ 1 ] ); 29 | Release = Marshal.GetDelegateForFunctionPointer( vtbl[ 2 ] ); 30 | Cache.Native.add( ptr, this ); 31 | } 32 | 33 | /// GUID of the COM interface 34 | public readonly Guid iid; 35 | 36 | /// Read the complete virtual methods table from the COM interface pointer. 37 | /// Native COM pointer 38 | /// Count of methods in the C# interface. The COM interface has 3 more methods from IUnknown. 39 | protected internal static IntPtr[] readVirtualTable( IntPtr nativePointer, int methodsCount ) 40 | { 41 | IntPtr vtbl = Marshal.ReadIntPtr( nativePointer ); 42 | int count = methodsCount + 3; 43 | 44 | IntPtr[] result = new IntPtr[ count ]; 45 | Marshal.Copy( vtbl, result, 0, count ); 46 | return result; 47 | } 48 | 49 | /// Release native COM pointer. If it reaches 0, causes C++ to run `delete this`. Safe to be called multiple times, only the first one will work. 50 | public void releaseInterfacePointer() 51 | { 52 | if( m_nativePointer != IntPtr.Zero ) 53 | { 54 | Cache.Native.drop( m_nativePointer, this ); 55 | Release( m_nativePointer ); 56 | m_nativePointer = IntPtr.Zero; 57 | } 58 | } 59 | 60 | /// Release in finalizer. 61 | ~RuntimeClass() 62 | { 63 | releaseInterfacePointer(); 64 | } 65 | 66 | void IDisposable.Dispose() 67 | { 68 | releaseInterfacePointer(); 69 | GC.SuppressFinalize( this ); 70 | } 71 | 72 | internal IntPtr queryInterface( Guid iid, bool addRef ) 73 | { 74 | int hr = QueryInterface( m_nativePointer, ref iid, out IntPtr result ); 75 | ErrorCodes.throwForHR( hr ); 76 | 77 | if( !addRef ) 78 | Release( m_nativePointer ); 79 | return result; 80 | } 81 | 82 | internal void addRef() 83 | { 84 | AddRef( m_nativePointer ); 85 | } 86 | 87 | internal void release() 88 | { 89 | Release( m_nativePointer ); 90 | } 91 | 92 | /// True if this proxy has a native COM pointer. Native pointers are released when you call IDisposable.Dispose() 93 | internal bool isAlive() 94 | { 95 | return m_nativePointer != IntPtr.Zero; 96 | } 97 | } 98 | } -------------------------------------------------------------------------------- /ComLight/Utils/EmitUtils.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | using System.Reflection.Emit; 4 | 5 | namespace ComLight 6 | { 7 | static class EmitUtils 8 | { 9 | static readonly OpCode[] intConstants = new OpCode[ 9 ] 10 | { 11 | OpCodes.Ldc_I4_0, OpCodes.Ldc_I4_1, OpCodes.Ldc_I4_2, OpCodes.Ldc_I4_3, OpCodes.Ldc_I4_4, OpCodes.Ldc_I4_5, OpCodes.Ldc_I4_6, OpCodes.Ldc_I4_7, OpCodes.Ldc_I4_8 12 | }; 13 | 14 | /// Push a constant int32 on the stack 15 | public static void pushIntConstant( this ILGenerator il, int i ) 16 | { 17 | if( i >= 0 && i <= 8 ) 18 | il.Emit( intConstants[ i ] ); 19 | else if( i >= -128 && i <= 127 ) 20 | il.Emit( OpCodes.Ldc_I4_S, (sbyte)i ); 21 | else 22 | il.Emit( OpCodes.Ldc_I4, i ); 23 | } 24 | 25 | static readonly OpCode[] ldArgOpcodes = new OpCode[ 4 ] 26 | { 27 | OpCodes.Ldarg_0, OpCodes.Ldarg_1, OpCodes.Ldarg_2, OpCodes.Ldarg_3 28 | }; 29 | 30 | /// Load argument to the stack, by index 31 | public static void loadArg( this ILGenerator il, int idx ) 32 | { 33 | if( idx < 0 || idx >= 0x10000 ) 34 | throw new ArgumentOutOfRangeException(); 35 | if( idx < 4 ) 36 | il.Emit( ldArgOpcodes[ idx ] ); 37 | else if( idx < 0x100 ) 38 | il.Emit( OpCodes.Ldarg_S, (byte)idx ); 39 | else 40 | { 41 | ushort us = (ushort)idx; 42 | short ss = unchecked((short)us); 43 | il.Emit( OpCodes.Ldarg, ss ); 44 | } 45 | } 46 | 47 | public static TypeBuilder emitStaticClass( this ModuleBuilder moduleBuilder, string name ) 48 | { 49 | TypeAttributes attr = TypeAttributes.Class | 50 | TypeAttributes.Abstract | TypeAttributes.AnsiClass | TypeAttributes.Sealed | TypeAttributes.AutoClass | TypeAttributes.BeforeFieldInit; 51 | return moduleBuilder.DefineType( name, attr ); 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /ComLight/Utils/ErrorCodes.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | using System.Runtime.InteropServices; 3 | 4 | namespace ComLight 5 | { 6 | /// Support a few extra HRESULT codes missing from non-Windows versions of .NET Core 7 | public static partial class ErrorCodes 8 | { 9 | /// If the argument SUCCEEDED, do nothing. If it FAILED, throw an exception, such as , resolving that code into message. 10 | /// Very similar to but supports more codes. 11 | [MethodImpl( MethodImplOptions.AggressiveInlining )] 12 | public static void throwForHR( int hr ) 13 | { 14 | if( hr >= 0 ) 15 | return; // SUCCEEDED 16 | string msg; 17 | if( codes.TryGetValue( hr, out msg ) ) 18 | throw new COMException( msg, hr ); 19 | Marshal.ThrowExceptionForHR( hr ); 20 | } 21 | 22 | /// If the argument SUCCEEDED, interpret the value as boolean, 0 = S_OK = true, anything else = false. If FAILED, throw an exception, such as , resolving that code into message. 23 | [MethodImpl( MethodImplOptions.AggressiveInlining )] 24 | public static bool throwAndReturnBool( int hr ) 25 | { 26 | if( hr >= 0 ) 27 | return 0 == hr; 28 | string msg; 29 | if( codes.TryGetValue( hr, out msg ) ) 30 | throw new COMException( msg, hr ); 31 | Marshal.ThrowExceptionForHR( hr ); 32 | return false; 33 | } 34 | 35 | /// Try to resolve HRESULT code into message. Returns null is not resolved. 36 | public static string tryLookupCode( int hr ) 37 | { 38 | return codes.lookup( hr ); 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /ComLight/Utils/ManagedWrapperCache.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.CompilerServices; 3 | 4 | namespace ComLight 5 | { 6 | /// A weakly-referenced cache with T keys, and tuples of ( IntPtr, Impl ) values 7 | /// Used to implement wrappers around C# streams passed to C++ methods 8 | sealed class ManagedWrapperCache where T : class where Impl : class 9 | { 10 | readonly object syncRoot = new object(); 11 | 12 | /// Values cached in this class 13 | public sealed class Entry 14 | { 15 | public readonly IntPtr nativePointer; 16 | readonly Impl impl; 17 | 18 | public Entry( IntPtr nativePointer, Impl impl ) 19 | { 20 | this.nativePointer = nativePointer; 21 | this.impl = impl; 22 | } 23 | } 24 | 25 | readonly ConditionalWeakTable native = new ConditionalWeakTable(); 26 | readonly Func factory; 27 | 28 | public ManagedWrapperCache( Func f ) 29 | { 30 | factory = f; 31 | } 32 | 33 | public IntPtr wrap( T managed, bool addRef ) 34 | { 35 | if( null == managed ) 36 | return IntPtr.Zero; 37 | 38 | lock( syncRoot ) 39 | { 40 | Entry entry; 41 | if( native.TryGetValue( managed, out entry ) ) 42 | { 43 | if( addRef ) 44 | Cache.Managed.addRef( entry.nativePointer ); 45 | return entry.nativePointer; 46 | } 47 | entry = factory( managed, addRef ); 48 | native.Add( managed, entry ); 49 | return entry.nativePointer; 50 | } 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /ComLight/Utils/MiscUtils.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq.Expressions; 4 | 5 | namespace ComLight 6 | { 7 | static class MiscUtils 8 | { 9 | public static TValue lookup( this Dictionary dict, TKey key ) 10 | { 11 | if( dict.TryGetValue( key, out TValue v ) ) 12 | return v; 13 | return default( TValue ); 14 | } 15 | 16 | public static bool isEmpty( this T[] arr ) 17 | { 18 | return null == arr || arr.Length <= 0; 19 | } 20 | 21 | public static bool notEmpty( this T[] arr ) 22 | { 23 | return arr != null && arr.Length > 0; 24 | } 25 | 26 | public static bool isEmpty( this ICollection list ) 27 | { 28 | return null == list || list.Count <= 0; 29 | } 30 | 31 | public static bool notEmpty( this ICollection list ) 32 | { 33 | return list != null && list.Count > 0; 34 | } 35 | 36 | /// ConstantExpression with boolean value `true` 37 | public static readonly ConstantExpression eTrue = Expression.Constant( true, typeof( bool ) ); 38 | 39 | /// ConstantExpression with boolean value `false` 40 | public static readonly ConstantExpression eFalse = Expression.Constant( false, typeof( bool ) ); 41 | 42 | /// 'ref IntPtr' type 43 | public static readonly Type intPtrRef = typeof( IntPtr ).MakeByRefType(); 44 | 45 | /// ConstantExpression with integer value S_OK 46 | public static readonly ConstantExpression S_OK = Expression.Constant( IUnknown.S_OK, typeof( int ) ); 47 | 48 | /// ConstantExpression with integer value S_FALSE 49 | public static readonly ConstantExpression S_FALSE = Expression.Constant( IUnknown.S_FALSE, typeof( int ) ); 50 | 51 | /// ConstantExpression with integer value E_UNEXPECTED 52 | public static readonly ConstantExpression E_UNEXPECTED = Expression.Constant( IUnknown.E_UNEXPECTED, typeof( int ) ); 53 | 54 | /// ConstantExpression with IntPtr value IntPtr.Zero 55 | public static readonly ConstantExpression nullptr = Expression.Constant( IntPtr.Zero, typeof( IntPtr ) ); 56 | 57 | public static Type[] noTypes = new Type[ 0 ]; 58 | 59 | /// If the argument is `ref something`, return that something. Otherwise return the argument. 60 | public static Type unwrapRef( this Type tp ) 61 | { 62 | if( !tp.IsByRef ) 63 | return tp; 64 | return tp.GetElementType(); 65 | } 66 | 67 | public static T getTarget( this WeakReference wr ) where T : class 68 | { 69 | T result; 70 | if( wr.TryGetTarget( out result ) ) 71 | return result; 72 | return null; 73 | } 74 | 75 | public static bool isDead( this WeakReference wr ) where T : class 76 | { 77 | return !wr.TryGetTarget( out T unused ); 78 | } 79 | } 80 | } -------------------------------------------------------------------------------- /ComLight/Utils/NativeWrapperCache.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace ComLight 5 | { 6 | class NativeWrapperCache where T : class 7 | { 8 | readonly object syncRoot = new object(); 9 | readonly Dictionary> instances = new Dictionary>(); 10 | readonly Func factory; 11 | 12 | public NativeWrapperCache( Func f ) 13 | { 14 | factory = f; 15 | } 16 | 17 | public T wrap( IntPtr nativeComPointer ) 18 | { 19 | if( nativeComPointer == IntPtr.Zero ) 20 | return null; 21 | 22 | WeakReference wr; 23 | T result; 24 | lock( syncRoot ) 25 | { 26 | if( instances.TryGetValue( nativeComPointer, out wr ) ) 27 | { 28 | if( wr.TryGetTarget( out result ) ) 29 | return result; 30 | } 31 | result = factory( nativeComPointer ); 32 | if( null == wr ) 33 | { 34 | wr = new WeakReference( result ); 35 | instances.Add( nativeComPointer, wr ); 36 | } 37 | else 38 | wr.SetTarget( result ); 39 | return result; 40 | } 41 | } 42 | 43 | public void dropIfDead( IntPtr p ) 44 | { 45 | lock( syncRoot ) 46 | { 47 | WeakReference wr; 48 | if( !instances.TryGetValue( p, out wr ) ) 49 | return; 50 | if( wr.isDead() ) 51 | instances.Remove( p ); 52 | } 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /ComLight/Utils/ReflectionUtils.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | 6 | namespace ComLight 7 | { 8 | static class ReflectionUtils 9 | { 10 | static bool notPropertyMethod( MethodInfo mi ) 11 | { 12 | if( !mi.Attributes.HasFlag( MethodAttributes.SpecialName ) ) 13 | return true; 14 | if( mi.Name.StartsWith( "get_" ) || mi.Name.StartsWith( "set_" ) ) 15 | return false; 16 | return true; 17 | } 18 | 19 | public static IEnumerable getMethodsWithoutProperties( this Type tp ) 20 | { 21 | return tp.GetMethods().Where( notPropertyMethod ); 22 | } 23 | 24 | /// Verify COM interface is OK, return it's GUID value. 25 | public static Guid checkInterface( Type tp ) 26 | { 27 | if( !tp.IsInterface ) 28 | throw new ArgumentException( $"Marshaler type argument { tp.FullName } is not an interface" ); 29 | 30 | if( !tp.IsPublic ) 31 | { 32 | // Proxies are implemented in different assembly, a dynamic one, they need access to the interface 33 | throw new ArgumentException( $"COM interface { tp.FullName } is not public" ); 34 | } 35 | 36 | if( tp.IsGenericType || tp.IsConstructedGenericType ) 37 | { 38 | throw new ArgumentException( $"COM interface { tp.FullName } is generic, this is not supported" ); 39 | } 40 | 41 | ComInterfaceAttribute attribute = tp.GetCustomAttribute(); 42 | if( null == attribute ) 43 | throw new ArgumentException( $"COM interface { tp.FullName } doesn't have [ComInterface] attribute applied" ); 44 | 45 | foreach( var m in tp.getMethodsWithoutProperties() ) 46 | ParamsMarshalling.checkInterfaceMethod( m ); 47 | 48 | return attribute.iid; 49 | } 50 | 51 | /// true if the type inherits from System.Delegate 52 | public static bool isDelegate( this Type tp ) 53 | { 54 | return typeof( Delegate ).IsAssignableFrom( tp ); 55 | } 56 | 57 | /// true if the type has specific custom attribute applied 58 | public static bool hasCustomAttribute( this Type tp ) where T : Attribute 59 | { 60 | return null != tp.GetCustomAttribute(); 61 | } 62 | 63 | /// true if the parameter has specific custom attribute applied 64 | public static bool hasCustomAttribute( this ParameterInfo pi ) where T : Attribute 65 | { 66 | return null != pi.GetCustomAttribute(); 67 | } 68 | 69 | public static bool hasRetValIndex( this MethodInfo mi ) 70 | { 71 | return null != mi.GetCustomAttribute(); 72 | } 73 | } 74 | } -------------------------------------------------------------------------------- /ComLight/Utils/errorCodez.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace ComLight 4 | { 5 | // This part of the class is automatically generated by a T4 template, by parsing C headers in ComLightLib and in Windows SDK 6 | public static partial class ErrorCodes 7 | { 8 | static readonly Dictionary codes = new Dictionary() 9 | { 10 | { unchecked( (int)0x80040007 ), "Uninitialized object" }, // OLE_E_BLANK 11 | { unchecked( (int)0x8000000B ), "The operation attempted to access data outside the valid range" }, // E_BOUNDS 12 | { unchecked( (int)0x80070026 ), "Attempted to read past the end of the stream" }, // E_EOF 13 | { unchecked( (int)0x800704DF ), "An attempt was made to perform an initialization operation when initialization has already been completed" }, // E_ALREADY_INITIALIZED 14 | }; 15 | } 16 | } -------------------------------------------------------------------------------- /ComLight/Utils/errorCodez.tt: -------------------------------------------------------------------------------- 1 | <#@ template debug="false" hostspecific="true" language="C#" #> 2 | <#@ assembly name="System.Core" #> 3 | <#@ assembly name="System.Text.RegularExpressions" #> 4 | <#@ import namespace="System.Linq" #> 5 | <#@ import namespace="System.Text" #> 6 | <#@ import namespace="System.IO" #> 7 | <#@ import namespace="System.Collections.Generic" #> 8 | <#@ import namespace="System.Text.RegularExpressions" #> 9 | <#@ import namespace="System.Runtime.InteropServices" #> 10 | <#@ import namespace="Microsoft.VisualStudio.TextTemplating" #> 11 | <#@ output extension=".cs" #>using System.Collections.Generic; 12 | 13 | namespace ComLight 14 | { 15 | // This part of the class is automatically generated by a T4 template, by parsing C headers in ComLightLib and in Windows SDK 16 | public static partial class ErrorCodes 17 | { 18 | static readonly Dictionary codes = new Dictionary()<# 19 | string dirNativeProject = Host.ResolvePath( "../../ComLightLib" ); 20 | parseErrorCodes( dirNativeProject, this ); 21 | #>; 22 | } 23 | }<#+ 24 | // Parsing C headers with regular expressions. What could possibly go wrong? 25 | 26 | const string pathWinError = @"C:\Program Files (x86)\Windows Kits\10\Include\10.0.17763.0\shared\winerror.h"; 27 | 28 | // Read a text file line by line, return non-empty lines 29 | static IEnumerable readFile( string path ) 30 | { 31 | using( var fs = File.OpenRead( path ) ) 32 | using( var sr = new StreamReader( fs ) ) 33 | { 34 | while( true ) 35 | { 36 | string line = sr.ReadLine(); 37 | if( null == line ) 38 | yield break; 39 | if( string.IsNullOrWhiteSpace( line ) ) 40 | continue; 41 | yield return line; 42 | } 43 | } 44 | } 45 | 46 | // Try to match string against a regular expression which captures 2 groups. If matched, return true and get the groups. 47 | static bool tryMatchRegEx( string line, Regex re, out string key, out string val ) 48 | { 49 | var m = re.Match( line ); 50 | if( !m.Success ) 51 | { 52 | key = null; 53 | val = null; 54 | return false; 55 | } 56 | key = m.Groups[ 1 ].Value; 57 | val = m.Groups[ 2 ].Value; 58 | return true; 59 | } 60 | 61 | // Match e.g. "constexpr HRESULT OLE_E_BLANK = _HRESULT_TYPEDEF_( 0x80040007 );", capture OLE_E_BLANK and 80040007 62 | static readonly Regex reHresultHex = new Regex( @"constexpr\s+HRESULT\s+(\S+)\s*=\s*_HRESULT_TYPEDEF_\s*\(\s*0x([0-9A-Fa-f]+)[Ll]?\s*\)\s*;" ); 63 | // Match e.g. "constexpr HRESULT E_EOF = HRESULT_FROM_WIN32( ERROR_HANDLE_EOF );", capture E_EOF and ERROR_HANDLE_EOF 64 | static readonly Regex reHresultWin32 = new Regex( @"constexpr\s+HRESULT\s+(\S+)\s*=\s*HRESULT_FROM_WIN32\s*\(\s*(\S+)\s*\)\s*;" ); 65 | 66 | class Error 67 | { 68 | public readonly string message, symbol; 69 | public Error( string m, string s ) { message = m; symbol = s; } 70 | } 71 | 72 | static void addError( Dictionary dict, int hr, string symbol ) 73 | { 74 | // Too lazy to parse error text from Windows headers. 75 | // This T4 runs on my PC, it's Windows 10 with en-us system locale, so I have FormatMessage API available and it retruns good enough messages. 76 | // FormatMessage API is exposed to .NET as Marshal.GetExceptionForHR, among others. 77 | Exception ex = Marshal.GetExceptionForHR( hr ); 78 | dict.Add( hr, new Error( ex.Message, symbol ) ); 79 | } 80 | 81 | static int HRESULT_FROM_WIN32( int w32 ) 82 | { 83 | if( w32 < 0 ) 84 | return w32; 85 | return ( w32 & 0xFFFF ) | unchecked( ( int ) 0x80070000 ); 86 | } 87 | 88 | static void tryParseCode( string line, Dictionary result ) 89 | { 90 | string k, v; 91 | if( tryMatchRegEx( line, reHresultHex, out k, out v ) ) 92 | { 93 | addError( result, Convert.ToInt32( v, 16 ), k ); 94 | return; 95 | } 96 | if( tryMatchRegEx( line, reHresultWin32, out k, out v ) ) 97 | { 98 | if( !winError.ContainsKey( v ) ) 99 | throw new KeyNotFoundException( v ); 100 | addError( result, HRESULT_FROM_WIN32( winError[ v ] ), k ); 101 | return; 102 | } 103 | } 104 | 105 | // Match e.g. "#define ERROR_ACCESS_DENIED 5L", capture ERROR_ACCESS_DENIED and 5 106 | static readonly Regex reWinError = new Regex( @"^#define\s+(\S+)\s+([0-9]+)[Ll]?\s*$" ); 107 | 108 | // Parse winerror.h SDK header, produce dictionary where key = symbol, value = numeric code 109 | static Dictionary parseWinError() 110 | { 111 | var res = new Dictionary(); 112 | foreach( string line in readFile( pathWinError ) ) 113 | { 114 | string k, v; 115 | if( tryMatchRegEx( line, reWinError, out k, out v ) ) 116 | res.Add( k, int.Parse( v ) ); 117 | } 118 | return res; 119 | } 120 | static readonly Dictionary winError = parseWinError(); 121 | 122 | // Slightly fix error messages, e.g. we don't want "(Exception from HRESULT: ...)" part 123 | static string makeErrorMessage( string str ) 124 | { 125 | int idx = str.IndexOf( "(Exception from HRESULT:" ); 126 | if( idx > 0 ) 127 | str = str.Substring( 0, idx ); 128 | return str.Trim().TrimEnd( '.', ' ', '\t' ); 129 | } 130 | 131 | static void parseErrorCodes( string dir, TextTransformation p ) 132 | { 133 | var codes = new Dictionary(); 134 | 135 | foreach( var path in Directory.GetFiles( dir, "*.h", SearchOption.AllDirectories ) ) 136 | { 137 | foreach( string line in readFile( path ) ) 138 | { 139 | tryParseCode( line, codes ); 140 | } 141 | } 142 | if( codes.Count <= 0 ) 143 | { 144 | p.Write( "{ }" ); 145 | return; 146 | } 147 | p.WriteLine( "" ); 148 | p.WriteLine( "\t\t{" ); 149 | foreach( var kvp in codes ) 150 | p.WriteLine( "\t\t\t{{ unchecked( (int)0x{0:X} ), \"{1}\" }}, // {2}", kvp.Key, makeErrorMessage( kvp.Value.message ), kvp.Value.symbol ); 151 | p.Write( "\t\t}" ); 152 | } 153 | #> -------------------------------------------------------------------------------- /ComLight/iComDisposable.cs: -------------------------------------------------------------------------------- 1 | namespace ComLight 2 | { 3 | /// Implement this interface to get notified when C++ code releases the last native reference to your object. 4 | public interface iComDisposable 5 | { 6 | /// Called when C++ code calls IUnknown.Release(), and the count of references to this object from native code reaches zero. 7 | /// This is a good place to dispose resources consumed by C++ code. 8 | void lastNativeReferenceReleased(); 9 | } 10 | } -------------------------------------------------------------------------------- /ComLightDesktop/Cache/Native.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using WeakRef = System.WeakReference; 5 | using WeakRefSet = System.Collections.Generic.HashSet>; 6 | 7 | namespace ComLight.Cache 8 | { 9 | // Desktop version of ConditionalWeakTable doesn't implement IEnumerable. Implementing same functionality using much slower code, with a hash set of weak references. 10 | // Let's hope not too many people are going to use the desktop version of this library, MS .NET built-in COM interop ain't that bad after all. 11 | static class Native 12 | { 13 | static readonly object syncRoot = new object(); 14 | static readonly Dictionary native = new Dictionary(); 15 | 16 | public static void add( IntPtr p, RuntimeClass rc ) 17 | { 18 | Debug.Assert( p != IntPtr.Zero ); 19 | 20 | lock( syncRoot ) 21 | { 22 | WeakRefSet set; 23 | if( !native.TryGetValue( p, out set ) ) 24 | { 25 | set = new WeakRefSet(); 26 | native.Add( p, set ); 27 | } 28 | set.Add( new WeakRef( rc ) ); 29 | } 30 | } 31 | 32 | static readonly List deadRefs = new List(); 33 | 34 | public static bool drop( IntPtr p, RuntimeClass rc ) 35 | { 36 | lock( syncRoot ) 37 | { 38 | WeakRefSet set; 39 | if( !native.TryGetValue( p, out set ) ) 40 | return false; 41 | 42 | bool found = false; 43 | foreach( WeakRef wr in set ) 44 | { 45 | if( !wr.TryGetTarget( out RuntimeClass r ) ) 46 | { 47 | deadRefs.Add( wr ); 48 | continue; 49 | } 50 | 51 | if( r == rc ) 52 | { 53 | found = true; 54 | deadRefs.Add( wr ); 55 | } 56 | else if( !rc.isAlive() ) 57 | deadRefs.Add( wr ); 58 | } 59 | 60 | foreach( var wr in deadRefs ) 61 | set.Remove( wr ); 62 | deadRefs.Clear(); 63 | 64 | if( set.Count <= 0 ) 65 | native.Remove( p ); 66 | 67 | return found; 68 | } 69 | } 70 | 71 | public static RuntimeClass lookup( IntPtr p, Type tInterface ) 72 | { 73 | lock( syncRoot ) 74 | { 75 | WeakRefSet set; 76 | if( !native.TryGetValue( p, out set ) ) 77 | return null; 78 | 79 | foreach( WeakRef wr in set ) 80 | { 81 | if( !wr.TryGetTarget( out RuntimeClass rc ) ) 82 | continue; 83 | if( !rc.isAlive() ) 84 | continue; 85 | if( !tInterface.IsAssignableFrom( rc.GetType() ) ) 86 | continue; 87 | return rc; 88 | } 89 | } 90 | return null; 91 | } 92 | } 93 | } -------------------------------------------------------------------------------- /ComLightDesktop/IO/StreamExt.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers; 3 | using System.IO; 4 | 5 | namespace ComLight 6 | { 7 | /// Extension methods for Stream to support read/write using Span<byte> This makes stream API compatible with .NET Core version, albeit slower due to extra copy. 8 | /// 9 | public static class StreamExt 10 | { 11 | /// Read bytes from stream into span 12 | public static int Read( this Stream thisStream, Span buffer ) 13 | { 14 | byte[] sharedBuffer = ArrayPool.Shared.Rent( buffer.Length ); 15 | try 16 | { 17 | int numRead = thisStream.Read( sharedBuffer, 0, buffer.Length ); 18 | if( (uint)numRead > (uint)buffer.Length ) 19 | throw new IOException( "Stream too long" ); 20 | new Span( sharedBuffer, 0, numRead ).CopyTo( buffer ); 21 | return numRead; 22 | } 23 | finally { ArrayPool.Shared.Return( sharedBuffer ); } 24 | } 25 | 26 | /// Write bytes from readonly span into stream 27 | public static void Write( this Stream thisStream, ReadOnlySpan buffer ) 28 | { 29 | byte[] sharedBuffer = ArrayPool.Shared.Rent( buffer.Length ); 30 | try 31 | { 32 | buffer.CopyTo( sharedBuffer ); 33 | thisStream.Write( sharedBuffer, 0, buffer.Length ); 34 | } 35 | finally { ArrayPool.Shared.Return( sharedBuffer ); } 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /ComLightDesktop/app.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /ComLightLib/ComLightLib.vcxproj.filters: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /ComLightLib/Exception.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace ComLight 4 | { 5 | class Exception : public std::runtime_error 6 | { 7 | // I don't like C++ exceptions too much, but for some cases they are useful. 8 | // You can throw ComLight::Exception from constructor, or from FinalConstruct() method, the library will catch & return the code from the class factory function. 9 | // Unfortunately, for interface methods this doesn't work, the C++ parts of the library can't catch them without very complex trickery like code generation. 10 | // You can still use this class in methods, but you'll need to catch them manually near the API boundary or the app will crash. 11 | // C++ doesn't have an ABI, the framework can't catch C++ exception across the modules. 12 | const HRESULT m_code; 13 | 14 | public: 15 | 16 | Exception( HRESULT hr ) : runtime_error( "ComLight HRESULT exception" ), m_code( hr ) { } 17 | 18 | HRESULT code() const { return m_code; } 19 | }; 20 | } -------------------------------------------------------------------------------- /ComLightLib/client/CComPtr.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace ComLight 4 | { 5 | // COM smart pointer, very comparable to CComPtr from ATL 6 | template 7 | class CComPtr 8 | { 9 | I* p; 10 | 11 | void callAddRef() const 12 | { 13 | if( nullptr == p ) 14 | return; 15 | p->AddRef(); 16 | } 17 | 18 | public: 19 | 20 | // Construct with nullptr 21 | CComPtr() : p( nullptr ) { } 22 | 23 | // Release the pointer 24 | void release() 25 | { 26 | if( nullptr == p ) 27 | return; 28 | p->Release(); 29 | p = nullptr; 30 | } 31 | 32 | ~CComPtr() 33 | { 34 | release(); 35 | } 36 | 37 | // Attach without AddRef() 38 | void attach( I* raw ) 39 | { 40 | release(); 41 | p = raw; 42 | } 43 | 44 | // Detach without Release(), set this pointer to nullptr 45 | I* detach() 46 | { 47 | I* const result = p; 48 | p = nullptr; 49 | return result; 50 | } 51 | 52 | // Detach without Release() and place to the specified address, set this pointer to nullptr 53 | template 54 | void detach( Other** pp ) 55 | { 56 | // If the argument points to a non-empty object, release the old instance: would leak memory otherwise. 57 | if( nullptr != *pp ) 58 | ( *pp )->Release(); 59 | ( *pp ) = detach(); 60 | } 61 | 62 | // Set and AddRef() 63 | void assign( I* raw ) 64 | { 65 | release(); 66 | attach( raw ); 67 | callAddRef(); 68 | } 69 | 70 | void swap( CComPtr& that ) 71 | { 72 | std::swap( p, that.p ); 73 | } 74 | 75 | // Set and AddRef() 76 | CComPtr( I* raw ) : p( raw ) 77 | { 78 | callAddRef(); 79 | } 80 | 81 | // Set and AddRef() 82 | CComPtr( const CComPtr& that ) : CComPtr( that.p ) { } 83 | // Move constructor 84 | CComPtr( CComPtr&& that ) : p( that.p ) { that.p = nullptr; } 85 | 86 | // Set and AddRef() 87 | void operator=( I* raw ) 88 | { 89 | assign( raw ); 90 | } 91 | 92 | // Set and AddRef() 93 | void operator=( const CComPtr& that ) 94 | { 95 | assign( that.p ); 96 | } 97 | 98 | // Move assignment operator, destroys the other one 99 | void operator=( CComPtr&& that ) 100 | { 101 | attach( that.detach() ); 102 | } 103 | 104 | operator I*( ) const { return p; } 105 | I* operator -> () const { return p; } 106 | I** operator &() { return &p; } 107 | 108 | operator bool() const { return nullptr != p; } 109 | }; 110 | } -------------------------------------------------------------------------------- /ComLightLib/comLightClient.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "comLightCommon.h" 3 | #include "client/CComPtr.hpp" 4 | #include "utils/typeTraits.hpp" 5 | 6 | namespace ComLight 7 | { 8 | namespace details 9 | { 10 | template 11 | inline constexpr void** castDoublePointerToVoid( T** pp ) 12 | { 13 | static_assert( pointersAssignable(), "IID_PPV_ARGS macro should be used with IUnknown interfaces" ); 14 | return reinterpret_cast( pp ); 15 | } 16 | } 17 | } 18 | 19 | #ifdef IID_PPV_ARGS 20 | #undef IID_PPV_ARGS 21 | #endif 22 | 23 | #define IID_PPV_ARGS( pp ) decltype( **pp )::iid, ::ComLight::details::castDoublePointerToVoid( pp ) -------------------------------------------------------------------------------- /ComLightLib/comLightCommon.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "hresult.h" 3 | 4 | #ifdef _MSC_VER 5 | #include 6 | #else 7 | #include "pal/guiddef.h" 8 | using LPCTSTR = const char*; 9 | #endif 10 | 11 | #include "unknwn.h" -------------------------------------------------------------------------------- /ComLightLib/comLightServer.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "comLightCommon.h" 3 | #include "client/CComPtr.hpp" 4 | 5 | #include "server/ObjectRoot.hpp" 6 | #include "server/interfaceMap.h" 7 | #include "server/Object.hpp" 8 | #include "server/freeThreadedMarshaller.h" 9 | 10 | #ifdef _MSC_VER 11 | // On Windows, it's controlled by library.def module definition file. There's __declspec(dllexport), but it adds underscore, I don't like that. 12 | #define DLLEXPORT extern "C" 13 | #else 14 | #define DLLEXPORT extern "C" __attribute__((visibility("default"))) 15 | #endif -------------------------------------------------------------------------------- /ComLightLib/hresult.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #ifdef _MSC_VER 4 | #include 5 | #include 6 | #else 7 | #include "pal/hresult.h" 8 | #endif 9 | 10 | #define CHECK( hr ) { const HRESULT __hr = ( hr ); if( FAILED( __hr ) ) return __hr; } 11 | 12 | #ifndef _MSC_VER 13 | inline constexpr HRESULT HRESULT_FROM_WIN32( int c ) 14 | { 15 | return c < 0 ? c : ( ( 0xFFFF & c ) | 0x80070000 ); 16 | } 17 | 18 | constexpr HRESULT OLE_E_BLANK = _HRESULT_TYPEDEF_( 0x80040007 ); 19 | constexpr HRESULT E_BOUNDS = _HRESULT_TYPEDEF_( 0x8000000BL ); 20 | 21 | constexpr int ERROR_HANDLE_EOF = 38; 22 | constexpr int ERROR_ALREADY_INITIALIZED = 1247; 23 | #endif 24 | 25 | constexpr HRESULT E_EOF = HRESULT_FROM_WIN32( ERROR_HANDLE_EOF ); 26 | constexpr HRESULT E_ALREADY_INITIALIZED = HRESULT_FROM_WIN32( ERROR_ALREADY_INITIALIZED ); -------------------------------------------------------------------------------- /ComLightLib/pal/guiddef.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #ifndef GUID_DEFINED 5 | #define GUID_DEFINED 6 | #endif 7 | 8 | struct GUID 9 | { 10 | uint32_t Data1; 11 | uint16_t Data2; 12 | uint16_t Data3; 13 | std::array Data4; 14 | 15 | constexpr inline bool operator==( const GUID& that ) const 16 | { 17 | return Data1 == that.Data1 && Data2 == that.Data2 && Data3 == that.Data3 && Data4 == that.Data4; 18 | } 19 | }; 20 | 21 | using REFIID = const GUID&; -------------------------------------------------------------------------------- /ComLightLib/pal/hresult.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | using HRESULT = int32_t; 4 | #define _HRESULT_TYPEDEF_(_sc) ((HRESULT)_sc) 5 | #define SEVERITY_ERROR 1 6 | #define FACILITY_CONTROL 10 7 | 8 | inline constexpr HRESULT MAKE_SCODE( uint32_t sev, uint32_t fac, uint32_t code ) 9 | { 10 | return (HRESULT)( ( (uint32_t)( sev ) << 31 ) | ( (unsigned long)( fac ) << 16 ) | ( (unsigned long)( code ) ) ); 11 | }; 12 | 13 | // ==== Copy-pasted from coreclr-master\src\pal\inc\rt\palrt.h ==== 14 | #define S_OK _HRESULT_TYPEDEF_(0x00000000L) 15 | #define S_FALSE _HRESULT_TYPEDEF_(0x00000001L) 16 | 17 | #define E_NOTIMPL _HRESULT_TYPEDEF_(0x80004001L) 18 | #define E_NOINTERFACE _HRESULT_TYPEDEF_(0x80004002L) 19 | #define E_UNEXPECTED _HRESULT_TYPEDEF_(0x8000FFFFL) 20 | #define E_OUTOFMEMORY _HRESULT_TYPEDEF_(0x8007000EL) 21 | #define E_INVALIDARG _HRESULT_TYPEDEF_(0x80070057L) 22 | #define E_POINTER _HRESULT_TYPEDEF_(0x80004003L) 23 | #define E_HANDLE _HRESULT_TYPEDEF_(0x80070006L) 24 | #define E_ABORT _HRESULT_TYPEDEF_(0x80004004L) 25 | #define E_FAIL _HRESULT_TYPEDEF_(0x80004005L) 26 | #define E_ACCESSDENIED _HRESULT_TYPEDEF_(0x80070005L) 27 | #define E_PENDING _HRESULT_TYPEDEF_(0x8000000AL) 28 | 29 | #define DISP_E_PARAMNOTFOUND _HRESULT_TYPEDEF_(0x80020004L) 30 | #define DISP_E_TYPEMISMATCH _HRESULT_TYPEDEF_(0x80020005L) 31 | #define DISP_E_BADVARTYPE _HRESULT_TYPEDEF_(0x80020008L) 32 | #define DISP_E_OVERFLOW _HRESULT_TYPEDEF_(0x8002000AL) 33 | #define DISP_E_DIVBYZERO _HRESULT_TYPEDEF_(0x80020012L) 34 | 35 | #define CLASS_E_CLASSNOTAVAILABLE _HRESULT_TYPEDEF_(0x80040111L) 36 | #define CLASS_E_NOAGGREGATION _HRESULT_TYPEDEF_(0x80040110L) 37 | 38 | #define CO_E_CLASSSTRING _HRESULT_TYPEDEF_(0x800401F3L) 39 | 40 | #define MK_E_SYNTAX _HRESULT_TYPEDEF_(0x800401E4L) 41 | 42 | #define STG_E_INVALIDFUNCTION _HRESULT_TYPEDEF_(0x80030001L) 43 | #define STG_E_FILENOTFOUND _HRESULT_TYPEDEF_(0x80030002L) 44 | #define STG_E_PATHNOTFOUND _HRESULT_TYPEDEF_(0x80030003L) 45 | #define STG_E_WRITEFAULT _HRESULT_TYPEDEF_(0x8003001DL) 46 | #define STG_E_FILEALREADYEXISTS _HRESULT_TYPEDEF_(0x80030050L) 47 | #define STG_E_ABNORMALAPIEXIT _HRESULT_TYPEDEF_(0x800300FAL) 48 | 49 | #define NTE_BAD_UID _HRESULT_TYPEDEF_(0x80090001L) 50 | #define NTE_BAD_HASH _HRESULT_TYPEDEF_(0x80090002L) 51 | #define NTE_BAD_KEY _HRESULT_TYPEDEF_(0x80090003L) 52 | #define NTE_BAD_LEN _HRESULT_TYPEDEF_(0x80090004L) 53 | #define NTE_BAD_DATA _HRESULT_TYPEDEF_(0x80090005L) 54 | #define NTE_BAD_SIGNATURE _HRESULT_TYPEDEF_(0x80090006L) 55 | #define NTE_BAD_VER _HRESULT_TYPEDEF_(0x80090007L) 56 | #define NTE_BAD_ALGID _HRESULT_TYPEDEF_(0x80090008L) 57 | #define NTE_BAD_FLAGS _HRESULT_TYPEDEF_(0x80090009L) 58 | #define NTE_BAD_TYPE _HRESULT_TYPEDEF_(0x8009000AL) 59 | #define NTE_BAD_KEY_STATE _HRESULT_TYPEDEF_(0x8009000BL) 60 | #define NTE_BAD_HASH_STATE _HRESULT_TYPEDEF_(0x8009000CL) 61 | #define NTE_NO_KEY _HRESULT_TYPEDEF_(0x8009000DL) 62 | #define NTE_NO_MEMORY _HRESULT_TYPEDEF_(0x8009000EL) 63 | #define NTE_SIGNATURE_FILE_BAD _HRESULT_TYPEDEF_(0x8009001CL) 64 | #define NTE_FAIL _HRESULT_TYPEDEF_(0x80090020L) 65 | 66 | #define CRYPT_E_HASH_VALUE _HRESULT_TYPEDEF_(0x80091007L) 67 | 68 | #define TYPE_E_SIZETOOBIG _HRESULT_TYPEDEF_(0x800288C5L) 69 | #define TYPE_E_DUPLICATEID _HRESULT_TYPEDEF_(0x800288C6L) 70 | 71 | #define STD_CTL_SCODE(n) MAKE_SCODE(SEVERITY_ERROR, FACILITY_CONTROL, n) 72 | #define CTL_E_OVERFLOW STD_CTL_SCODE(6) 73 | #define CTL_E_OUTOFMEMORY STD_CTL_SCODE(7) 74 | #define CTL_E_DIVISIONBYZERO STD_CTL_SCODE(11) 75 | #define CTL_E_OUTOFSTACKSPACE STD_CTL_SCODE(28) 76 | #define CTL_E_FILENOTFOUND STD_CTL_SCODE(53) 77 | #define CTL_E_DEVICEIOERROR STD_CTL_SCODE(57) 78 | #define CTL_E_PERMISSIONDENIED STD_CTL_SCODE(70) 79 | #define CTL_E_PATHFILEACCESSERROR STD_CTL_SCODE(75) 80 | #define CTL_E_PATHNOTFOUND STD_CTL_SCODE(76) 81 | 82 | #define INET_E_CANNOT_CONNECT _HRESULT_TYPEDEF_(0x800C0004L) 83 | #define INET_E_RESOURCE_NOT_FOUND _HRESULT_TYPEDEF_(0x800C0005L) 84 | #define INET_E_OBJECT_NOT_FOUND _HRESULT_TYPEDEF_(0x800C0006L) 85 | #define INET_E_DATA_NOT_AVAILABLE _HRESULT_TYPEDEF_(0x800C0007L) 86 | #define INET_E_DOWNLOAD_FAILURE _HRESULT_TYPEDEF_(0x800C0008L) 87 | #define INET_E_CONNECTION_TIMEOUT _HRESULT_TYPEDEF_(0x800C000BL) 88 | #define INET_E_UNKNOWN_PROTOCOL _HRESULT_TYPEDEF_(0x800C000DL) 89 | 90 | #define DBG_PRINTEXCEPTION_C _HRESULT_TYPEDEF_(0x40010006L) 91 | // ==== Done pasting ==== 92 | 93 | inline constexpr bool SUCCEEDED( HRESULT hr ) 94 | { 95 | return hr >= 0; 96 | } 97 | 98 | inline constexpr bool FAILED( HRESULT hr ) 99 | { 100 | return hr < 0; 101 | } -------------------------------------------------------------------------------- /ComLightLib/server/Object.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include "../comLightClient.h" 4 | #include "../utils/typeTraits.hpp" 5 | #include "../Exception.hpp" 6 | 7 | namespace ComLight 8 | { 9 | namespace details 10 | { 11 | GENERATE_HAS_MEMBER( implQueryInterface ); 12 | GENERATE_HAS_MEMBER( implAddRef ); 13 | GENERATE_HAS_MEMBER( implRelease ); 14 | } 15 | 16 | // Outer class of objects, implements IUnknown methods, also the class factory. The type argument must be your class implementing your interfaces, inherited from ObjectRoot 17 | template 18 | class Object : public T 19 | { 20 | public: 21 | Object() = default; 22 | inline virtual ~Object() override { } 23 | 24 | // Implement IUnknown methods 25 | HRESULT COMLIGHTCALL QueryInterface( REFIID riid, void **ppvObject ) override 26 | { 27 | static_assert( details::has_member_implQueryInterface::value, "Your object class must inherit from ComLight::ObjectRoot" ); 28 | 29 | if( nullptr == ppvObject ) 30 | return E_POINTER; 31 | 32 | if( T::implQueryInterface( riid, ppvObject ) ) 33 | return S_OK; 34 | if( T::queryExtraInterfaces( riid, ppvObject ) ) 35 | return S_OK; 36 | 37 | if( riid == IUnknown::iid() ) 38 | { 39 | ComLight::IUnknown* unk = T::getUnknown(); 40 | unk->AddRef(); 41 | *ppvObject = unk; 42 | return S_OK; 43 | } 44 | 45 | return E_NOINTERFACE; 46 | } 47 | 48 | uint32_t COMLIGHTCALL AddRef() override 49 | { 50 | static_assert( details::has_member_implAddRef::value, "Your object class must inherit from ComLight::ObjectRoot" ); 51 | return T::implAddRef(); 52 | } 53 | 54 | uint32_t COMLIGHTCALL Release() override 55 | { 56 | static_assert( details::has_member_implRelease::value, "Your object class must inherit from ComLight::ObjectRoot" ); 57 | const uint32_t ret = T::implRelease(); 58 | if( 0 == ret ) 59 | { 60 | T::FinalRelease(); 61 | delete this; 62 | } 63 | return ret; 64 | } 65 | 66 | // Create a new object on the heap, store in smart pointer 67 | static inline HRESULT create( CComPtr>& result ) 68 | { 69 | CComPtr> ptr; 70 | try 71 | { 72 | ptr = new Object(); // The RefCounter constructor creates it with ref.counter 0. But then CComPtr constructor calls AddRef so we have RC=1 after this line. 73 | 74 | HRESULT hr = ptr->internalFinalConstruct(); 75 | if( FAILED( hr ) ) 76 | return hr; 77 | 78 | hr = ptr->FinalConstruct(); 79 | if( FAILED( hr ) ) 80 | return hr; 81 | 82 | ptr.swap( result ); 83 | return S_OK; 84 | } 85 | catch( const Exception& ex ) 86 | { 87 | return ex.code(); 88 | } 89 | } 90 | 91 | // Create a new object on the heap, return one of it's interfaces. The caller is assumed to take ownership of the new object. 92 | template 93 | static inline HRESULT create( I** pp ) 94 | { 95 | if( pp == nullptr ) 96 | return E_POINTER; 97 | 98 | static_assert( details::pointersAssignable(), "Object::create can't cast object to the requested interface" ); 99 | CComPtr> ptr; 100 | CHECK( create( ptr ) ); 101 | ptr.detach( pp ); 102 | return S_OK; 103 | } 104 | }; 105 | } -------------------------------------------------------------------------------- /ComLightLib/server/ObjectRoot.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "RefCounter.hpp" 3 | #include "../comLightCommon.h" 4 | #include "../utils/typeTraits.hpp" 5 | 6 | namespace ComLight 7 | { 8 | // Base class of objects, implements reference counting, also a few lifetime methods. 9 | // The template argument is the interface you want clients to get when they ask for IID_IUnknown. By convention, that pointer defines object's identity. 10 | template 11 | class ObjectRoot : public RefCounter, public I 12 | { 13 | protected: 14 | 15 | inline HRESULT internalFinalConstruct() 16 | { 17 | return S_FALSE; 18 | } 19 | 20 | inline HRESULT FinalConstruct() 21 | { 22 | return S_FALSE; 23 | } 24 | 25 | inline void FinalRelease() { } 26 | 27 | IUnknown* getUnknown() 28 | { 29 | static_assert( details::pointersAssignable(), "The interface doesn't derive from IUnknown" ); 30 | return static_cast( this ); 31 | } 32 | 33 | bool queryExtraInterfaces( REFIID riid, void **ppvObject ) const 34 | { 35 | return false; 36 | } 37 | 38 | // Implement query interface with 2 entries, IUnknown and I. 39 | bool implQueryInterface( REFIID riid, void** ppvObject ) 40 | { 41 | if( riid == I::iid() || riid == IUnknown::iid() ) 42 | { 43 | I* const result = this; 44 | result->AddRef(); 45 | *ppvObject = result; 46 | return true; 47 | } 48 | return false; 49 | } 50 | }; 51 | } -------------------------------------------------------------------------------- /ComLightLib/server/RefCounter.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | 6 | namespace ComLight 7 | { 8 | // Very base class of objects, implements reference counting. 9 | class RefCounter 10 | { 11 | std::atomic_uint referenceCounter; 12 | 13 | public: 14 | 15 | RefCounter() : referenceCounter( 0 ) { } 16 | 17 | inline virtual ~RefCounter() { } 18 | 19 | RefCounter( const RefCounter &that ) = delete; 20 | RefCounter( RefCounter &&that ) = delete; 21 | 22 | protected: 23 | 24 | uint32_t implAddRef() 25 | { 26 | return ++referenceCounter; 27 | } 28 | 29 | uint32_t implRelease() 30 | { 31 | // Might be a good idea to use locks, at least in debug builds. They're much slower than atomics, but with locks it's possible to detect when 2 threads call release at the same time, for object with counter = 1. 32 | // It's a memory management bug, but it would be nice if debug builds would handle that case gracefully. 33 | const uint32_t rc = --referenceCounter; 34 | assert( rc != UINT_MAX ); 35 | return rc; 36 | } 37 | }; 38 | } -------------------------------------------------------------------------------- /ComLightLib/server/freeThreadedMarshaller.cpp: -------------------------------------------------------------------------------- 1 | #include "freeThreadedMarshaller.h" 2 | #ifdef _MSC_VER 3 | #include 4 | 5 | HRESULT ComLight::details::createFreeThreadedMarshaller( IUnknown* pUnkOuter, IUnknown** ppUnkMarshal ) 6 | { 7 | return ::CoCreateFreeThreadedMarshaler( (LPUNKNOWN)pUnkOuter, (LPUNKNOWN *)ppUnkMarshal ); 8 | } 9 | 10 | bool ComLight::details::queryMarshallerInterface( REFIID riid, void **ppvObject, IUnknown* marshaller ) 11 | { 12 | if( riid != IID_IMarshal || nullptr == marshaller ) 13 | return false; 14 | const HRESULT hr = marshaller->QueryInterface( IID_IMarshal, ppvObject ); 15 | return SUCCEEDED( hr ) ? true : false; 16 | } 17 | #endif -------------------------------------------------------------------------------- /ComLightLib/server/freeThreadedMarshaller.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #ifdef _MSC_VER 3 | #include "../comLightCommon.h" 4 | 5 | namespace ComLight 6 | { 7 | namespace details 8 | { 9 | HRESULT createFreeThreadedMarshaller( IUnknown* pUnkOuter, IUnknown** ppUnkMarshal ); 10 | bool queryMarshallerInterface( REFIID riid, void **ppvObject, IUnknown* marshaller ); 11 | } 12 | } 13 | 14 | #define DECLARE_FREE_THREADED_MARSHALLER() \ 15 | private: \ 16 | ComLight::CComPtr m_freeThreadedMarshaller; \ 17 | protected: \ 18 | HRESULT internalFinalConstruct() \ 19 | { \ 20 | return ComLight::details::createFreeThreadedMarshaller( getUnknown(), &m_freeThreadedMarshaller ); \ 21 | } \ 22 | bool queryExtraInterfaces( REFIID riid, void **ppvObject ) const \ 23 | { \ 24 | return ComLight::details::queryMarshallerInterface( riid, ppvObject, m_freeThreadedMarshaller ); \ 25 | } 26 | 27 | #else 28 | #define DECLARE_FREE_THREADED_MARSHALLER() 29 | #endif -------------------------------------------------------------------------------- /ComLightLib/server/interfaceMap.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "../utils/typeTraits.hpp" 3 | 4 | // Unlike ATL, the interface map is optional for ComLight. 5 | // If you won't declare a map, the object will support 2 interfaces: IUnknown, and whatever template argument was passed to ObjectRoot class. 6 | #define BEGIN_COM_MAP() \ 7 | protected: \ 8 | bool implQueryInterface( REFIID iid, void** ppvObject ) { 9 | 10 | #define END_COM_MAP() return false; } 11 | 12 | namespace ComLight 13 | { 14 | namespace details 15 | { 16 | template 17 | inline bool tryReturnInterface( REFIID iid, C* pThis, void** ppvResult ) 18 | { 19 | static_assert( pointersAssignable(), "Trying to implement an interface that doesn't derive from IUnknown" ); 20 | static_assert( pointersAssignable(), "Declared support for an interface, but the class doesn't implement it" ); 21 | if( I::iid != iid ) 22 | return false; 23 | I* const result = pThis; 24 | result->AddRef(); 25 | *ppvResult = result; 26 | return true; 27 | } 28 | } 29 | } 30 | 31 | #define COM_INTERFACE_ENTRY( I ) if( ComLight::details::tryReturnInterface( iid, this, ppvObject ) ) return true; -------------------------------------------------------------------------------- /ComLightLib/streams.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include "comLightCommon.h" 4 | 5 | // COM interfaces to marshal streams across the interop. 6 | namespace ComLight 7 | { 8 | enum struct eSeekOrigin : uint8_t 9 | { 10 | Begin = 0, 11 | Current = 1, 12 | End = 2 13 | }; 14 | 15 | namespace details 16 | { 17 | template 18 | inline size_t sizeofVector( const std::vector& vec ) 19 | { 20 | return sizeof( E ) * vec.size(); 21 | } 22 | } 23 | 24 | // COM interface for readonly stream. You'll get these interfaces what you use [ReadStream] attribute in C#. 25 | struct DECLSPEC_NOVTABLE iReadStream : public IUnknown 26 | { 27 | DEFINE_INTERFACE_ID( "006af6db-734e-4595-8c94-19304b2389ac" ); 28 | 29 | virtual HRESULT COMLIGHTCALL read( void* lpBuffer, int nNumberOfBytesToRead, int &lpNumberOfBytesRead ) = 0; 30 | virtual HRESULT COMLIGHTCALL seek( int64_t offset, eSeekOrigin origin ) = 0; 31 | virtual HRESULT COMLIGHTCALL getPosition( int64_t& position ) = 0; 32 | virtual HRESULT COMLIGHTCALL getLength( int64_t& length ) = 0; 33 | 34 | template 35 | inline HRESULT read( std::vector& vec ) 36 | { 37 | const int cb = (int)details::sizeofVector( vec ); 38 | int cbRead = 0; 39 | CHECK( read( vec.data(), cb, cbRead ) ); 40 | if( cbRead >= cb ) 41 | return S_OK; 42 | return E_EOF; 43 | } 44 | }; 45 | 46 | // COM interface for readonly stream. You'll get these interfaces what you use [WriteStream] attribute in C#. 47 | struct DECLSPEC_NOVTABLE iWriteStream : public IUnknown 48 | { 49 | DEFINE_INTERFACE_ID( "d7c3eb39-9170-43b9-ba98-2ea1f2fed8a8" ); 50 | 51 | virtual HRESULT COMLIGHTCALL write( const void* lpBuffer, int nNumberOfBytesToWrite ) = 0; 52 | virtual HRESULT COMLIGHTCALL flush() = 0; 53 | 54 | template 55 | inline HRESULT write( const std::vector& vec ) 56 | { 57 | const int cb = (int)details::sizeofVector( vec ); 58 | return write( vec.data(), cb ); 59 | } 60 | }; 61 | } -------------------------------------------------------------------------------- /ComLightLib/unknwn.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | // Calling conventions 5 | #ifdef _MSC_VER 6 | #define COMLIGHTCALL __stdcall 7 | #define DECLSPEC_NOVTABLE __declspec(novtable) 8 | #elif defined(__GNUC__) || defined(__clang__) 9 | #if defined(__i386__) 10 | #define COMLIGHTCALL __attribute__((stdcall)) 11 | #else 12 | #define COMLIGHTCALL 13 | #endif 14 | #define DECLSPEC_NOVTABLE 15 | #else 16 | #error Unsupported C++ compiler 17 | #endif 18 | 19 | #include "utils/guid_parse.hpp" 20 | 21 | #define DEFINE_INTERFACE_ID( guidString ) static constexpr GUID iid() { return ::ComLight::make_guid( guidString ); } 22 | 23 | namespace ComLight 24 | { 25 | // This thing is binary compatible with IUnknown from Windows SDK. See DesktopClient demo project, it uses normal COM interop in .NET framework 4.7 to call my implementation. 26 | struct DECLSPEC_NOVTABLE IUnknown 27 | { 28 | DEFINE_INTERFACE_ID( "00000000-0000-0000-c000-000000000046" ); 29 | 30 | virtual HRESULT COMLIGHTCALL QueryInterface( REFIID riid, void **ppvObject ) = 0; 31 | 32 | virtual uint32_t COMLIGHTCALL AddRef() = 0; 33 | 34 | virtual uint32_t COMLIGHTCALL Release() = 0; 35 | }; 36 | } -------------------------------------------------------------------------------- /ComLightLib/utils/guid_parse.hpp: -------------------------------------------------------------------------------- 1 | // https://github.com/tobias-loew/constexpr-GUID-cpp-11 2 | 3 | //------------------------------------------------------------------------------------------------------- 4 | // constexpr GUID parsing 5 | // Written by Alexander Bessonov 6 | // Written by Tobias Loew 7 | // 8 | // Licensed under the MIT license. 9 | //------------------------------------------------------------------------------------------------------- 10 | 11 | #pragma once 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | #if !defined(GUID_DEFINED) 18 | #define GUID_DEFINED 19 | struct GUID { 20 | uint32_t Data1; 21 | uint16_t Data2; 22 | uint16_t Data3; 23 | uint8_t Data4[ 8 ]; 24 | }; 25 | #endif 26 | 27 | namespace ComLight 28 | { 29 | namespace details 30 | { 31 | constexpr const size_t short_guid_form_length = 36; // XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX 32 | constexpr const size_t long_guid_form_length = 38; // {XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX} 33 | 34 | constexpr uint8_t parse_hex_digit( const char c ) 35 | { 36 | using namespace std::string_literals; 37 | return 38 | ( '0' <= c && c <= '9' ) 39 | ? c - '0' 40 | : ( 'a' <= c && c <= 'f' ) 41 | ? 10 + c - 'a' 42 | : ( 'A' <= c && c <= 'F' ) 43 | ? 10 + c - 'A' 44 | : 45 | throw std::domain_error{ "invalid character in GUID"s }; 46 | } 47 | 48 | constexpr uint8_t parse_hex_uint8_t( const char *ptr ) 49 | { 50 | return ( parse_hex_digit( ptr[ 0 ] ) << 4 ) + parse_hex_digit( ptr[ 1 ] ); 51 | } 52 | 53 | constexpr uint16_t parse_hex_uint16_t( const char *ptr ) 54 | { 55 | return ( parse_hex_uint8_t( ptr ) << 8 ) + parse_hex_uint8_t( ptr + 2 ); 56 | } 57 | 58 | constexpr uint32_t parse_hex_uint32_t( const char *ptr ) 59 | { 60 | return ( parse_hex_uint16_t( ptr ) << 16 ) + parse_hex_uint16_t( ptr + 4 ); 61 | } 62 | 63 | constexpr GUID parse_guid( const char *begin ) 64 | { 65 | return GUID{ 66 | parse_hex_uint32_t( begin ), 67 | parse_hex_uint16_t( begin + 8 + 1 ), 68 | parse_hex_uint16_t( begin + 8 + 1 + 4 + 1 ), 69 | { 70 | parse_hex_uint8_t( begin + 8 + 1 + 4 + 1 + 4 + 1 ), 71 | parse_hex_uint8_t( begin + 8 + 1 + 4 + 1 + 4 + 1 + 2 ), 72 | parse_hex_uint8_t( begin + 8 + 1 + 4 + 1 + 4 + 1 + 2 + 2 + 1 ), 73 | parse_hex_uint8_t( begin + 8 + 1 + 4 + 1 + 4 + 1 + 2 + 2 + 1 + 2 ), 74 | parse_hex_uint8_t( begin + 8 + 1 + 4 + 1 + 4 + 1 + 2 + 2 + 1 + 2 + 2 ), 75 | parse_hex_uint8_t( begin + 8 + 1 + 4 + 1 + 4 + 1 + 2 + 2 + 1 + 2 + 2 + 2 ), 76 | parse_hex_uint8_t( begin + 8 + 1 + 4 + 1 + 4 + 1 + 2 + 2 + 1 + 2 + 2 + 2 + 2 ), 77 | parse_hex_uint8_t( begin + 8 + 1 + 4 + 1 + 4 + 1 + 2 + 2 + 1 + 2 + 2 + 2 + 2 + 2 ) 78 | } 79 | }; 80 | } 81 | 82 | constexpr GUID make_guid_helper( const char *str, size_t N ) 83 | { 84 | using namespace std::string_literals; 85 | using namespace details; 86 | 87 | return ( !( N == long_guid_form_length || N == short_guid_form_length ) ) 88 | ? throw std::domain_error{ "String GUID of the form {XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX} or XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX is expected"s } 89 | : ( N == long_guid_form_length && ( str[ 0 ] != '{' || str[ long_guid_form_length - 1 ] != '}' ) ) 90 | ? throw std::domain_error{ "Missing opening or closing brace"s } 91 | 92 | : parse_guid( str + ( N == long_guid_form_length ? 1 : 0 ) ); 93 | } 94 | 95 | 96 | template 97 | constexpr GUID make_guid( const char( &str )[ N ] ) 98 | { 99 | return make_guid_helper( str, N - 1 ); 100 | } 101 | } 102 | using details::make_guid; 103 | } -------------------------------------------------------------------------------- /ComLightLib/utils/typeTraits.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | namespace ComLight 5 | { 6 | namespace details 7 | { 8 | template 9 | constexpr bool pointersAssignable() 10 | { 11 | // See this for why `&` is required: https://stackoverflow.com/a/52429468/126995 12 | return std::is_assignable::value; 13 | } 14 | } 15 | } 16 | 17 | // https://en.wikibooks.org/wiki/More_C++_Idioms/Member_Detector 18 | #define GENERATE_HAS_MEMBER(member) \ 19 | \ 20 | template < class T > \ 21 | class HasMember_##member \ 22 | { \ 23 | private: \ 24 | using Yes = char[2]; \ 25 | using No = char[1]; \ 26 | \ 27 | struct Fallback { int member; }; \ 28 | struct Derived : T, Fallback { }; \ 29 | \ 30 | template < class U > \ 31 | static No& test ( decltype(U::member)* ); \ 32 | template < typename U > \ 33 | static Yes& test ( U* ); \ 34 | \ 35 | public: \ 36 | static constexpr bool RESULT = sizeof(test(nullptr)) == sizeof(Yes); \ 37 | }; \ 38 | \ 39 | template < class T > \ 40 | struct has_member_##member \ 41 | : public std::integral_constant::RESULT> \ 42 | { \ 43 | }; 44 | -------------------------------------------------------------------------------- /Demos/HelloWorldCS/HelloWorld.cs: -------------------------------------------------------------------------------- 1 | using ComLight; 2 | using System; 3 | using System.Runtime.InteropServices; 4 | 5 | // Declare an interface, must match to the C++ side of the interop 6 | [ComInterface( "cdc9e3c6-b300-4138-b006-c61e7c2bfe48" )] 7 | public interface IHelloWorld 8 | { 9 | bool print( string what ); 10 | } 11 | 12 | class Program 13 | { 14 | // Import class factory function from *.dll / *.so 15 | [DllImport( "helloworld", PreserveSig = false )] 16 | static extern void createHelloWorld( [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Marshaler ) )] out IHelloWorld obj ); 17 | 18 | static void Main( string[] args ) 19 | { 20 | // Call factory to create the object instance 21 | createHelloWorld( out IHelloWorld test ); 22 | // Call a method 23 | bool res = test.print( "Hello, World." ); 24 | Console.WriteLine( "Returned: {0}", res ); 25 | } 26 | } -------------------------------------------------------------------------------- /Demos/HelloWorldCS/HelloWorldCS.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net8.0 6 | false 7 | false 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /Demos/HelloWorldCpp/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required( VERSION 3.5 ) 2 | project( helloworld LANGUAGES CXX ) 3 | set( CMAKE_CXX_STANDARD 14 ) 4 | set( CMAKE_CXX_STANDARD_REQUIRED ON ) 5 | set( CMAKE_CXX_EXTENSIONS OFF ) 6 | set( CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fvisibility=hidden -fvisibility-inlines-hidden -Wall -Wno-psabi -march=native -O3" ) 7 | add_library( helloworld SHARED HelloWorld.cpp ) -------------------------------------------------------------------------------- /Demos/HelloWorldCpp/HelloWorld.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "../../ComLightLib/comLightServer.h" 3 | 4 | // Declare an interface 5 | struct DECLSPEC_NOVTABLE IHelloWorld : public ComLight::IUnknown 6 | { 7 | DEFINE_INTERFACE_ID( "{cdc9e3c6-b300-4138-b006-c61e7c2bfe48}" ); 8 | 9 | virtual HRESULT COMLIGHTCALL print( const char* msg ) = 0; 10 | }; 11 | 12 | // Implement the interface 13 | class HelloWorld : public ComLight::ObjectRoot 14 | { 15 | HRESULT COMLIGHTCALL print( const char* msg ) override 16 | { 17 | printf( "%s\n", msg ); 18 | return S_FALSE; 19 | } 20 | }; 21 | 22 | // Create class factory function. Registration parts of COM is not great, also not portable. Using C API instead. 23 | DLLEXPORT HRESULT COMLIGHTCALL createHelloWorld( IHelloWorld **pp ) 24 | { 25 | return ComLight::Object::create( pp ); 26 | } -------------------------------------------------------------------------------- /Demos/HelloWorldCpp/HelloWorld.def: -------------------------------------------------------------------------------- 1 | EXPORTS 2 | createHelloWorld -------------------------------------------------------------------------------- /Demos/HelloWorldCpp/HelloWorld.vcxproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | Win32 7 | 8 | 9 | Release 10 | Win32 11 | 12 | 13 | Debug 14 | x64 15 | 16 | 17 | Release 18 | x64 19 | 20 | 21 | 22 | 15.0 23 | {69C652FB-4300-494A-B1AC-53FAF518C913} 24 | Win32Proj 25 | HelloWorld 26 | 10.0 27 | HelloWorldCpp 28 | 29 | 30 | 31 | DynamicLibrary 32 | true 33 | v143 34 | Unicode 35 | 36 | 37 | DynamicLibrary 38 | false 39 | v143 40 | true 41 | Unicode 42 | 43 | 44 | DynamicLibrary 45 | true 46 | v143 47 | Unicode 48 | 49 | 50 | DynamicLibrary 51 | false 52 | v143 53 | true 54 | Unicode 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | true 76 | helloworld 77 | 78 | 79 | true 80 | helloworld 81 | 82 | 83 | false 84 | helloworld 85 | 86 | 87 | false 88 | helloworld 89 | 90 | 91 | 92 | NotUsing 93 | Level3 94 | Disabled 95 | true 96 | _DEBUG;HELLOWORLD_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) 97 | true 98 | 99 | 100 | Windows 101 | true 102 | HelloWorld.def 103 | 104 | 105 | 106 | 107 | NotUsing 108 | Level3 109 | Disabled 110 | true 111 | WIN32;_DEBUG;HELLOWORLD_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) 112 | true 113 | 114 | 115 | Windows 116 | true 117 | HelloWorld.def 118 | 119 | 120 | 121 | 122 | NotUsing 123 | Level3 124 | MaxSpeed 125 | true 126 | true 127 | true 128 | WIN32;NDEBUG;HELLOWORLD_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) 129 | true 130 | 131 | 132 | Windows 133 | true 134 | true 135 | true 136 | HelloWorld.def 137 | 138 | 139 | 140 | 141 | NotUsing 142 | Level3 143 | MaxSpeed 144 | true 145 | true 146 | true 147 | NDEBUG;HELLOWORLD_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) 148 | true 149 | 150 | 151 | Windows 152 | true 153 | true 154 | true 155 | HelloWorld.def 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | -------------------------------------------------------------------------------- /Demos/HelloWorldCpp/HelloWorld.vcxproj.filters: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Demos/StreamsCS/Streams.cs: -------------------------------------------------------------------------------- 1 | using ComLight; 2 | using System.IO; 3 | using System.Runtime.InteropServices; 4 | 5 | [ComInterface( "d29d85bf-d6d1-4c4c-8989-ce9260debc60" )] 6 | public interface iFileSystem 7 | { 8 | void openFile( [NativeString] string path, [ReadStream] out Stream stm ); 9 | void createFile( [NativeString] string path, [WriteStream] out Stream stm ); 10 | } 11 | 12 | class ManagedFileSystem: iFileSystem 13 | { 14 | void iFileSystem.createFile( string path, out Stream stm ) 15 | { 16 | stm = File.Create( path ); 17 | } 18 | void iFileSystem.openFile( string path, out Stream stm ) 19 | { 20 | stm = File.OpenRead( path ); 21 | } 22 | } 23 | 24 | [ComInterface( "0d30d69c-c9f5-40f1-b16b-77f54de38805" )] 25 | public interface iStreamsDemo 26 | { 27 | void init( iFileSystem managed, out iFileSystem native ); 28 | 29 | void copyWithManaged( [NativeString] string pathFrom, [NativeString] string pathTo ); 30 | } 31 | 32 | class Program 33 | { 34 | // Import class factory function from *.dll / *.so 35 | [DllImport( "streams", PreserveSig = false )] 36 | static extern void createStreams( [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Marshaler ) )] out iStreamsDemo obj ); 37 | 38 | static void copyWithNative( iFileSystem nativeFs, string pathFrom, string pathTo ) 39 | { 40 | Stream from, to; 41 | nativeFs.openFile( pathFrom, out from ); 42 | using( from ) 43 | { 44 | nativeFs.createFile( pathTo, out to ); 45 | using( to ) 46 | from.CopyTo( to ); 47 | } 48 | } 49 | 50 | static void Main( string[] args ) 51 | { 52 | createStreams( out iStreamsDemo demo ); 53 | iFileSystem managedFs = new ManagedFileSystem(); 54 | demo.init( managedFs, out iFileSystem nativeFs ); 55 | 56 | copyWithNative( nativeFs, @"C:\Temp\bases.jpg", @"C:\Temp\bases-copy.jpg" ); 57 | demo.copyWithManaged( @"C:\Temp\bases.jpg", @"C:\Temp\bases-copy-2.jpg" ); 58 | } 59 | } -------------------------------------------------------------------------------- /Demos/StreamsCS/StreamsCS.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net8.0 6 | false 7 | false 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /Demos/StreamsCpp/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required( VERSION 3.5 ) 2 | project( streams LANGUAGES CXX ) 3 | set( CMAKE_CXX_STANDARD 14 ) 4 | set( CMAKE_CXX_STANDARD_REQUIRED ON ) 5 | set( CMAKE_CXX_EXTENSIONS OFF ) 6 | set( CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fvisibility=hidden -fvisibility-inlines-hidden -Wall -Wno-psabi -march=native -O3" ) 7 | add_library( streams SHARED NativeFileSystem.cpp Streams.cpp ) -------------------------------------------------------------------------------- /Demos/StreamsCpp/NativeFileSystem.cpp: -------------------------------------------------------------------------------- 1 | #include "interfaces.h" 2 | 3 | class NativeFileSystem::ReadStream : public ObjectRoot 4 | { 5 | HRESULT COMLIGHTCALL read( void* lpBuffer, int nNumberOfBytesToRead, int &lpNumberOfBytesRead ) override 6 | { 7 | if( nullptr == m_file ) 8 | return OLE_E_BLANK; 9 | const size_t cb = fread( lpBuffer, 1, (size_t)nNumberOfBytesToRead, m_file ); 10 | lpNumberOfBytesRead = (int)cb; 11 | return S_OK; 12 | } 13 | 14 | HRESULT COMLIGHTCALL seek( int64_t offset, eSeekOrigin origin ) override { return E_NOTIMPL; } 15 | 16 | HRESULT COMLIGHTCALL getPosition( int64_t& position ) override 17 | { 18 | if( nullptr == m_file ) 19 | return OLE_E_BLANK; 20 | position = (int64_t)ftell( m_file ); 21 | return S_OK; 22 | } 23 | 24 | HRESULT COMLIGHTCALL getLength( int64_t& length ) override 25 | { 26 | if( nullptr == m_file ) 27 | return OLE_E_BLANK; 28 | fseek( m_file, 0, SEEK_END ); 29 | length = (int64_t)ftell( m_file ); 30 | fseek( m_file, 0, SEEK_SET ); 31 | return S_OK; 32 | } 33 | 34 | FILE* m_file = nullptr; 35 | 36 | public: 37 | 38 | HRESULT openFile( LPCTSTR path ) 39 | { 40 | #ifdef _MSC_VER 41 | const auto e = _wfopen_s( &m_file, path, L"rb" ); 42 | return ( 0 == e ) ? S_OK : CTL_E_DEVICEIOERROR; 43 | #else 44 | m_file = fopen( path, "rb" ); 45 | return ( nullptr != m_file ) ? S_OK : CTL_E_DEVICEIOERROR; 46 | #endif 47 | } 48 | }; 49 | 50 | HRESULT COMLIGHTCALL NativeFileSystem::openFile( LPCTSTR path, iReadStream** pp ) 51 | { 52 | CComPtr> stm; 53 | CHECK( Object::create( stm ) ); 54 | CHECK( stm->openFile( path ) ); 55 | stm.detach( pp ); 56 | return S_OK; 57 | } 58 | 59 | class NativeFileSystem::WriteStream : public ObjectRoot 60 | { 61 | HRESULT COMLIGHTCALL write( const void* lpBuffer, int nNumberOfBytesToWrite ) override 62 | { 63 | if( nullptr == m_file ) 64 | return OLE_E_BLANK; 65 | const size_t cb = (size_t)nNumberOfBytesToWrite; 66 | const size_t written = fwrite( lpBuffer, 1, cb, m_file ); 67 | if( cb == written ) 68 | return S_OK; 69 | return CTL_E_DEVICEIOERROR; 70 | } 71 | 72 | HRESULT COMLIGHTCALL flush() override 73 | { 74 | if( nullptr == m_file ) 75 | return OLE_E_BLANK; 76 | if( 0 == fflush( m_file ) ) 77 | return S_OK; 78 | return CTL_E_DEVICEIOERROR; 79 | } 80 | 81 | FILE* m_file = nullptr; 82 | 83 | public: 84 | 85 | WriteStream() = default; 86 | 87 | HRESULT createFile( LPCTSTR path ) 88 | { 89 | #ifdef _MSC_VER 90 | const auto e = _wfopen_s( &m_file, path, L"wb" ); 91 | return ( 0 == e ) ? S_OK : CTL_E_DEVICEIOERROR; 92 | #else 93 | m_file = fopen( path, "wb" ); 94 | return ( nullptr != m_file ) ? S_OK : CTL_E_DEVICEIOERROR; 95 | #endif 96 | } 97 | }; 98 | 99 | HRESULT COMLIGHTCALL NativeFileSystem::createFile( LPCTSTR path, iWriteStream** pp ) 100 | { 101 | CComPtr> stm; 102 | CHECK( Object::create( stm ) ); 103 | CHECK( stm->createFile( path ) ); 104 | stm.detach( pp ); 105 | return S_OK; 106 | } -------------------------------------------------------------------------------- /Demos/StreamsCpp/Streams.cpp: -------------------------------------------------------------------------------- 1 | #include "interfaces.h" 2 | #include 3 | 4 | class StreamsDemo : public ComLight::ObjectRoot 5 | { 6 | CComPtr m_managed; 7 | 8 | HRESULT COMLIGHTCALL init( iFileSystem* managed, iFileSystem** ppNative ) override 9 | { 10 | m_managed = managed; 11 | return ComLight::Object::create( ppNative ); 12 | } 13 | 14 | HRESULT COMLIGHTCALL copyWithManaged( LPCTSTR pathFrom, LPCTSTR pathTo ) override 15 | { 16 | CComPtr read; 17 | CHECK( m_managed->openFile( pathFrom, &read ) ); 18 | 19 | CComPtr write; 20 | CHECK( m_managed->createFile( pathTo, &write ) ); 21 | 22 | constexpr int cbBuffer = 1024; 23 | std::array buffer; 24 | 25 | while( true ) 26 | { 27 | int cb; 28 | CHECK( read->read( buffer.data(), cbBuffer, cb ) ); 29 | if( 0 == cb ) 30 | return write->flush(); 31 | CHECK( write->write( buffer.data(), cb ) ); 32 | } 33 | } 34 | }; 35 | 36 | DLLEXPORT HRESULT COMLIGHTCALL createStreams( iStreamsDemo **pp ) 37 | { 38 | return ComLight::Object::create( pp ); 39 | } -------------------------------------------------------------------------------- /Demos/StreamsCpp/Streams.def: -------------------------------------------------------------------------------- 1 | EXPORTS 2 | createStreams -------------------------------------------------------------------------------- /Demos/StreamsCpp/Streams.vcxproj.filters: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /Demos/StreamsCpp/interfaces.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "../../ComLightLib/comLightServer.h" 3 | #include "../../ComLightLib/streams.h" 4 | using namespace ComLight; 5 | 6 | struct DECLSPEC_NOVTABLE iFileSystem : public ComLight::IUnknown 7 | { 8 | DEFINE_INTERFACE_ID( "{d29d85bf-d6d1-4c4c-8989-ce9260debc60}" ); 9 | 10 | virtual HRESULT COMLIGHTCALL openFile( LPCTSTR path, iReadStream** stm ) = 0; 11 | virtual HRESULT COMLIGHTCALL createFile( LPCTSTR path, iWriteStream** stm ) = 0; 12 | }; 13 | 14 | // You probably don't want to implement file streams in C++ in production code, especially not with , I/O is way better in .NET. 15 | // But for a demo, streams are trivially easy to use, and are cross platform. 16 | class NativeFileSystem : public ObjectRoot 17 | { 18 | class ReadStream; 19 | class WriteStream; 20 | HRESULT COMLIGHTCALL openFile( LPCTSTR path, iReadStream** stm ) override; 21 | HRESULT COMLIGHTCALL createFile( LPCTSTR path, iWriteStream** stm ) override; 22 | }; 23 | 24 | struct DECLSPEC_NOVTABLE iStreamsDemo : public ComLight::IUnknown 25 | { 26 | DEFINE_INTERFACE_ID( "0d30d69c-c9f5-40f1-b16b-77f54de38805" ); 27 | 28 | virtual HRESULT COMLIGHTCALL init( iFileSystem* managed, iFileSystem** ppNative ) = 0; 29 | 30 | virtual HRESULT COMLIGHTCALL copyWithManaged( LPCTSTR pathFrom, LPCTSTR pathTo ) = 0; 31 | }; -------------------------------------------------------------------------------- /Demos/StreamsDesktop/App.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /Demos/StreamsDesktop/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle( "StreamsDesktop" )] 9 | [assembly: AssemblyDescription( "" )] 10 | [assembly: AssemblyConfiguration( "" )] 11 | [assembly: AssemblyCompany( "" )] 12 | [assembly: AssemblyProduct( "StreamsDesktop" )] 13 | [assembly: AssemblyCopyright( "Copyright © 2019" )] 14 | [assembly: AssemblyTrademark( "" )] 15 | [assembly: AssemblyCulture( "" )] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible( false )] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid( "3b25f687-15c7-4e6f-8085-1357b99e2fa9" )] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion( "1.0.0.0" )] 36 | [assembly: AssemblyFileVersion( "1.0.0.0" )] 37 | -------------------------------------------------------------------------------- /Demos/StreamsDesktop/StreamsDesktop.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {3B25F687-15C7-4E6F-8085-1357B99E2FA9} 8 | Exe 9 | StreamsDesktop 10 | StreamsDesktop 11 | v4.7.2 12 | 512 13 | true 14 | true 15 | 16 | 17 | AnyCPU 18 | true 19 | full 20 | false 21 | bin\Debug\ 22 | DEBUG;TRACE 23 | prompt 24 | 4 25 | false 26 | 27 | 28 | AnyCPU 29 | pdbonly 30 | true 31 | bin\Release\ 32 | TRACE 33 | prompt 34 | 4 35 | false 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | Streams.cs 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | {19900a21-4167-40b0-ab73-eb4bd4a78c1c} 59 | ComLightDesktop 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /DesktopClient/App.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /DesktopClient/DesktopClient.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {E37D2BA1-878A-4E41-BDD3-18B44B239569} 8 | Exe 9 | DesktopClient 10 | DesktopClient 11 | v4.7.2 12 | 512 13 | true 14 | true 15 | 16 | 17 | AnyCPU 18 | true 19 | full 20 | false 21 | ..\x64\Debug\ 22 | DEBUG;TRACE 23 | prompt 24 | 4 25 | 7.1 26 | false 27 | 28 | 29 | AnyCPU 30 | pdbonly 31 | true 32 | ..\x64\Release\ 33 | TRACE 34 | prompt 35 | 4 36 | 7.1 37 | false 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /DesktopClient/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Runtime.InteropServices; 4 | using System.Threading.Tasks; 5 | 6 | namespace DesktopClient 7 | { 8 | [InterfaceType( ComInterfaceType.InterfaceIsIUnknown ), Guid( "a3ccc418-1565-47dc-88ab-78dcfb5cc800" )] 9 | interface ITest 10 | { 11 | void add( int a, int b, out int result ); 12 | } 13 | 14 | class Program 15 | { 16 | public const string dll = "comtest"; 17 | 18 | [DllImport( dll, PreserveSig = false )] 19 | static extern void createTest( out ITest obj ); 20 | 21 | const int DISP_E_OVERFLOW = unchecked((int)0x8002000A); 22 | 23 | static async Task Main( string[] args ) 24 | { 25 | ITest test = null; 26 | try 27 | { 28 | createTest( out test ); 29 | int r = -1; 30 | Action act = () => test.add( 1, 2, out r ); 31 | await Task.Run( act ); 32 | Debug.Assert( r == 3 ); 33 | Console.WriteLine( "Result: {0}", r ); 34 | test.add( int.MinValue, int.MinValue, out r ); 35 | } 36 | catch( Exception ex ) 37 | { 38 | Debug.Assert( ex.HResult == DISP_E_OVERFLOW ); 39 | Console.WriteLine( "Exception: {0}", ex.Message ); 40 | } 41 | Marshal.FinalReleaseComObject( test ); 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /DesktopClient/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle( "DesktopClient" )] 9 | [assembly: AssemblyDescription( "" )] 10 | [assembly: AssemblyConfiguration( "" )] 11 | [assembly: AssemblyCompany( "" )] 12 | [assembly: AssemblyProduct( "DesktopClient" )] 13 | [assembly: AssemblyCopyright( "Copyright © 2019" )] 14 | [assembly: AssemblyTrademark( "" )] 15 | [assembly: AssemblyCulture( "" )] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible( false )] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid( "e37d2ba1-878a-4e41-bdd3-18b44b239569" )] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion( "1.0.0.0" )] 36 | [assembly: AssemblyFileVersion( "1.0.0.0" )] 37 | -------------------------------------------------------------------------------- /DesktopTest/App.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /DesktopTest/DesktopTest.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {5020041D-744D-40E5-87C1-028D39A346DC} 8 | Exe 9 | DesktopTest 10 | DesktopTest 11 | v4.7.2 12 | 512 13 | true 14 | true 15 | 16 | 17 | AnyCPU 18 | true 19 | full 20 | false 21 | ..\x64\Debug\ 22 | DEBUG;TRACE 23 | prompt 24 | 4 25 | false 26 | 27 | 28 | AnyCPU 29 | pdbonly 30 | true 31 | ..\x64\Release\ 32 | TRACE 33 | prompt 34 | 4 35 | false 36 | 37 | 38 | 39 | 40 | ..\packages\System.Buffers.4.5.0\lib\netstandard2.0\System.Buffers.dll 41 | 42 | 43 | 44 | ..\packages\System.Memory.4.5.3\lib\netstandard2.0\System.Memory.dll 45 | 46 | 47 | 48 | ..\packages\System.Numerics.Vectors.4.4.0\lib\net46\System.Numerics.Vectors.dll 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | ITest.cs 60 | 61 | 62 | ManagedImpl.cs 63 | 64 | 65 | Tests.cs 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | {19900a21-4167-40b0-ab73-eb4bd4a78c1c} 76 | ComLightDesktop 77 | 78 | 79 | 80 | 81 | 4.5.1 82 | 83 | 84 | 4.5.5 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /DesktopTest/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace DesktopTest 4 | { 5 | class Program 6 | { 7 | static void Main( string[] args ) 8 | { 9 | try 10 | { 11 | // test0(); 12 | // test1(); 13 | Tests.testStream(); 14 | // Tests.testMarshalBack(); 15 | } 16 | catch( Exception ex ) 17 | { 18 | Console.WriteLine( ex.ToString() ); 19 | } 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /DesktopTest/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle( "DesktopTest" )] 9 | [assembly: AssemblyDescription( "" )] 10 | [assembly: AssemblyConfiguration( "" )] 11 | [assembly: AssemblyCompany( "" )] 12 | [assembly: AssemblyProduct( "DesktopTest" )] 13 | [assembly: AssemblyCopyright( "Copyright © 2019" )] 14 | [assembly: AssemblyTrademark( "" )] 15 | [assembly: AssemblyCulture( "" )] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible( false )] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid( "5020041d-744d-40e5-87c1-028d39a346dc" )] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion( "1.0.0.0" )] 36 | [assembly: AssemblyFileVersion( "1.0.0.0" )] 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Konstantin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /NativeLibrary/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required( VERSION 3.5 ) 2 | project( cpp-demo LANGUAGES CXX ) 3 | 4 | set( CMAKE_CXX_STANDARD 14 ) 5 | set( CMAKE_CXX_STANDARD_REQUIRED ON ) 6 | set( CMAKE_CXX_EXTENSIONS OFF ) 7 | set( CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fvisibility=hidden -fvisibility-inlines-hidden -Wall -Wno-psabi" ) 8 | 9 | # https://stackoverflow.com/q/46724267/126995 10 | include( CheckCXXCompilerFlag ) 11 | CHECK_CXX_COMPILER_FLAG( "-march=native" COMPILER_SUPPORTS_MARCH_NATIVE ) 12 | if( COMPILER_SUPPORTS_MARCH_NATIVE ) 13 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -march=native") 14 | endif() 15 | 16 | CHECK_CXX_COMPILER_FLAG( "-O3" COMPILER_SUPPORTS_O3 ) 17 | if( COMPILER_SUPPORTS_O3 ) 18 | set( CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O3" ) 19 | endif() 20 | 21 | add_library( comtest SHARED Test.cpp ) -------------------------------------------------------------------------------- /NativeLibrary/ITest.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "../ComLightLib/streams.h" 3 | 4 | struct DECLSPEC_NOVTABLE ITest : public ComLight::IUnknown 5 | { 6 | DEFINE_INTERFACE_ID( "{a3ccc418-1565-47dc-88ab-78dcfb5cc800}" ); 7 | 8 | virtual HRESULT COMLIGHTCALL add( int a, int b, int& result ) = 0; 9 | 10 | virtual HRESULT COMLIGHTCALL addManaged( ITest* pManaged, int a, int b, int& result ) = 0; 11 | 12 | virtual HRESULT COMLIGHTCALL testPerformance( ITest* pManaged, int& result, double& elapsedSeconds ) = 0; 13 | 14 | virtual HRESULT COMLIGHTCALL testStreams( ComLight::iReadStream* stmRead, ComLight::iWriteStream* stmWrite ) = 0; 15 | 16 | virtual HRESULT COMLIGHTCALL createFile( LPCTSTR path, ComLight::iWriteStream** pp ) = 0; 17 | 18 | virtual HRESULT COMLIGHTCALL testMarshalBack( LPCTSTR path, ITest* pManaged ) = 0; 19 | }; 20 | 21 | // Another interface, just for testing the library 22 | struct DECLSPEC_NOVTABLE ITest2 : public ComLight::IUnknown 23 | { 24 | DEFINE_INTERFACE_ID( "{ffbed1a8-cd1a-4586-8916-3dc61dc7701e}" ); 25 | }; -------------------------------------------------------------------------------- /NativeLibrary/NativeLibrary.vcxproj.filters: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /NativeLibrary/Test.cpp: -------------------------------------------------------------------------------- 1 | #include "stdafx.h" 2 | #include "Test.h" 3 | #include 4 | #include 5 | #include 6 | #include "WriteStream.h" 7 | 8 | HRESULT COMLIGHTCALL Test::add( int a, int b, int& result ) 9 | { 10 | int64_t res = (int64_t)a + (int64_t)b; 11 | if( res < INT_MIN || res > INT_MAX ) 12 | return DISP_E_OVERFLOW; 13 | result = (int)res; 14 | return S_OK; 15 | } 16 | 17 | HRESULT COMLIGHTCALL Test::addManaged( ITest* pManaged, int a, int b, int& result ) 18 | { 19 | return pManaged->add( a, b, result ); 20 | } 21 | 22 | HRESULT COMLIGHTCALL Test::testPerformance( ITest* pManaged, int& result, double& elapsedSeconds ) 23 | { 24 | std::vector values; 25 | values.resize( 1000000 ); 26 | 27 | // https://stackoverflow.com/a/19666713/126995 28 | std::random_device rd; 29 | std::mt19937 mt( rd() ); 30 | std::uniform_int_distribution dist( 0, 0x40000000 ); 31 | for( int& v : values ) 32 | v = dist( mt ); 33 | 34 | int x = 0; 35 | const auto start = std::chrono::high_resolution_clock::now(); 36 | for( int i: values ) 37 | { 38 | int r; 39 | pManaged->add( i, i, r ); 40 | x ^= r; 41 | } 42 | const auto finish = std::chrono::high_resolution_clock::now(); 43 | std::chrono::duration elapsed = finish - start; 44 | result = x; 45 | elapsedSeconds = elapsed.count(); 46 | return S_OK; 47 | } 48 | 49 | HRESULT COMLIGHTCALL Test::testStreams( ComLight::iReadStream* stmRead, ComLight::iWriteStream* stmWrite ) 50 | { 51 | int64_t len; 52 | CHECK( stmRead->getLength( len ) ); 53 | 54 | std::vector vec; 55 | vec.resize( (size_t)len ); 56 | CHECK( stmRead->seek( 0, ComLight::eSeekOrigin::Begin ) ); 57 | CHECK( stmRead->read( vec ) ); 58 | CHECK( stmWrite->write( vec ) ); 59 | return S_OK; 60 | } 61 | 62 | HRESULT COMLIGHTCALL Test::createFile( LPCTSTR path, ComLight::iWriteStream** pp ) 63 | { 64 | if( nullptr == pp ) 65 | return E_POINTER; 66 | using namespace ComLight; 67 | CComPtr> stm; 68 | CHECK( Object::create( stm ) ); 69 | CHECK( stm->createFile( path ) ); 70 | stm.detach( pp ); 71 | return S_OK; 72 | } 73 | 74 | HRESULT COMLIGHTCALL Test::testMarshalBack( LPCTSTR path, ITest *pManaged ) 75 | { 76 | if( nullptr == pManaged ) 77 | return E_POINTER; 78 | 79 | using namespace ComLight; 80 | CComPtr stm; 81 | CHECK( pManaged->createFile( path, &stm ) ); 82 | const char* hw = "Hello, world."; 83 | CHECK( stm->write( hw, (int)strlen( hw ) ) ); 84 | return S_OK; 85 | } 86 | 87 | DLLEXPORT HRESULT COMLIGHTCALL createTest( ITest **pp ) 88 | { 89 | return ComLight::Object::create( pp ); 90 | } -------------------------------------------------------------------------------- /NativeLibrary/Test.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "ITest.h" 3 | #include "../ComLightLib/comLightServer.h" 4 | 5 | class Test: public ComLight::ObjectRoot, public ITest2 6 | { 7 | HRESULT COMLIGHTCALL add( int a, int b, int& result ) override; 8 | 9 | HRESULT COMLIGHTCALL addManaged( ITest* pManaged, int a, int b, int& result ) override; 10 | 11 | HRESULT COMLIGHTCALL testPerformance( ITest* pManaged, int& result, double& elapsedSeconds ) override; 12 | 13 | HRESULT COMLIGHTCALL testStreams( ComLight::iReadStream* stmRead, ComLight::iWriteStream* stmWrite ) override; 14 | 15 | HRESULT COMLIGHTCALL createFile( LPCTSTR path, ComLight::iWriteStream** pp ) override; 16 | 17 | HRESULT COMLIGHTCALL testMarshalBack( LPCTSTR path, ITest * pManaged ) override; 18 | 19 | // This line is only required if you want to consume the object from desktop .NET framework, and call it from multiple threads. See this for more info: https://stackoverflow.com/a/34978626/126995 20 | DECLARE_FREE_THREADED_MARSHALLER() 21 | 22 | // Unlike ATL, the interface map is optional for ComLight. 23 | // If you won't declare a map, the object will support 2 interfaces: IUnknown, and whatever template argument was passed to ObjectRoot class. 24 | // Interface map is only required to support multiple COM interfaces on the same object. 25 | 26 | /* BEGIN_COM_MAP() 27 | COM_INTERFACE_ENTRY( ITest ) 28 | COM_INTERFACE_ENTRY( ITest2 ) 29 | END_COM_MAP() */ 30 | }; 31 | 32 | DLLEXPORT HRESULT COMLIGHTCALL createTest( ITest **pp ); -------------------------------------------------------------------------------- /NativeLibrary/WriteStream.cpp: -------------------------------------------------------------------------------- 1 | #include "stdafx.h" 2 | #include "WriteStream.h" 3 | 4 | HRESULT WriteStream::write( const void* lpBuffer, int nNumberOfBytesToWrite ) 5 | { 6 | if( nullptr == m_file ) 7 | return OLE_E_BLANK; 8 | if( nNumberOfBytesToWrite < 0 ) 9 | return E_INVALIDARG; 10 | const size_t cb = (size_t)nNumberOfBytesToWrite; 11 | const size_t written = fwrite( lpBuffer, 1, cb, m_file ); 12 | if( cb == written ) 13 | return S_OK; 14 | return CTL_E_DEVICEIOERROR; 15 | } 16 | 17 | HRESULT WriteStream::flush() 18 | { 19 | if( nullptr == m_file ) 20 | return OLE_E_BLANK; 21 | if( 0 == fflush( m_file ) ) 22 | return S_OK; 23 | return CTL_E_DEVICEIOERROR; 24 | } 25 | 26 | HRESULT WriteStream::createFile( LPCTSTR path ) 27 | { 28 | if( nullptr != m_file ) 29 | { 30 | fclose( m_file ); 31 | m_file = nullptr; 32 | } 33 | #ifdef _MSC_VER 34 | const auto e = _wfopen_s( &m_file, path, L"wb" ); 35 | return ( 0 == e ) ? S_OK : CTL_E_DEVICEIOERROR; 36 | #else 37 | m_file = fopen( path, "wb" ); 38 | return ( nullptr != m_file ) ? S_OK : CTL_E_DEVICEIOERROR; 39 | #endif 40 | } -------------------------------------------------------------------------------- /NativeLibrary/WriteStream.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "../ComLightLib/comLightServer.h" 3 | #include "../ComLightLib/streams.h" 4 | #include 5 | 6 | // iWriteStream implementation over file handle. 7 | class WriteStream : public ComLight::ObjectRoot 8 | { 9 | HRESULT write( const void* lpBuffer, int nNumberOfBytesToWrite ) override; 10 | 11 | HRESULT flush() override; 12 | 13 | FILE* m_file = nullptr; 14 | 15 | public: 16 | 17 | WriteStream() = default; 18 | 19 | HRESULT createFile( LPCTSTR path ); 20 | }; -------------------------------------------------------------------------------- /NativeLibrary/dllmain.cpp: -------------------------------------------------------------------------------- 1 | #include "stdafx.h" 2 | 3 | BOOL APIENTRY DllMain( HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved ) 4 | { 5 | switch( ul_reason_for_call ) 6 | { 7 | case DLL_PROCESS_ATTACH: 8 | case DLL_THREAD_ATTACH: 9 | case DLL_THREAD_DETACH: 10 | case DLL_PROCESS_DETACH: 11 | break; 12 | } 13 | return TRUE; 14 | } -------------------------------------------------------------------------------- /NativeLibrary/library.def: -------------------------------------------------------------------------------- 1 | EXPORTS 2 | createTest -------------------------------------------------------------------------------- /NativeLibrary/stdafx.cpp: -------------------------------------------------------------------------------- 1 | #include "stdafx.h" 2 | -------------------------------------------------------------------------------- /NativeLibrary/stdafx.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #ifdef _MSC_VER 3 | 4 | #include "targetver.h" 5 | 6 | #define WIN32_LEAN_AND_MEAN 7 | #include 8 | #endif 9 | #include -------------------------------------------------------------------------------- /NativeLibrary/targetver.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // Including SDKDDKVer.h defines the highest available Windows platform. 4 | 5 | // If you wish to build your application for a previous Windows platform, include WinSDKVer.h and 6 | // set the _WIN32_WINNT macro to the platform you wish to support before including SDKDDKVer.h. 7 | 8 | #include 9 | -------------------------------------------------------------------------------- /PortableClient/ITest.cs: -------------------------------------------------------------------------------- 1 | using ComLight; 2 | using System; 3 | using System.IO; 4 | using System.Runtime.InteropServices; 5 | 6 | [ComInterface( "a3ccc418-1565-47dc-88ab-78dcfb5cc800" )] 7 | public interface ITest: IDisposable 8 | { 9 | // Just add 2 numbers 10 | int add( int a, int b, [Out] out int result ); 11 | 12 | // Add 2 numbers by calling ITest.add on the supplied COM interface, testing native to managed marshaling. 13 | void addManaged( ITest managed, int a, int b, [Out] out int result ); 14 | 15 | // Add 1M random numbers in native code, by calling the ITest.add on the supplied interface. 16 | void testPerformance( ITest managed, out int xor, out double seconds ); 17 | 18 | // Does the same as Stream.CopyTo, implemented across the interop boundary. 19 | void testStreams( [ReadStream] Stream stmRead, [WriteStream] Stream stmWrite ); 20 | 21 | // Create a file for writing, return the stream 22 | void createFile( [NativeString] string str, [WriteStream] out Stream stmWrite ); 23 | 24 | // Create a file for writing by calling ITest.createFile on the supplied interface, write hello world there. 25 | void testMarshalBack( [NativeString] string str, ITest managed ); 26 | } -------------------------------------------------------------------------------- /PortableClient/LinuxUtils.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Reflection; 4 | using System.Runtime.InteropServices; 5 | 6 | namespace PortableClient 7 | { 8 | static class LinuxUtils 9 | { 10 | private const int RTLD_LAZY = 0x00001; //Only resolve symbols as needed 11 | private const int RTLD_GLOBAL = 0x00100; //Make symbols available to libraries loaded later 12 | 13 | [DllImport( "dl" )] 14 | static extern IntPtr dlopen( string file, int mode ); 15 | 16 | public static bool preloadDll( string nameDll ) 17 | { 18 | var exe = Assembly.GetExecutingAssembly().Location; 19 | string dir = Path.GetDirectoryName( exe ); 20 | string pathDll = Path.Combine( dir, nameDll ); 21 | if( !File.Exists( pathDll ) ) 22 | return false; 23 | IntPtr res = dlopen( pathDll, RTLD_LAZY | RTLD_GLOBAL ); 24 | if( res != IntPtr.Zero ) 25 | Console.WriteLine( "Preloaded the DLL from \"{0}\"", pathDll ); 26 | else 27 | Console.WriteLine( "dlopen failed to load the DLL from \"{0}\"", pathDll ); 28 | return true; 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /PortableClient/ManagedImpl.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | 4 | class ManagedImpl: ITest 5 | { 6 | int ITest.add( int a, int b, out int result ) 7 | { 8 | try 9 | { 10 | checked 11 | { 12 | result = a + b; 13 | return 0; 14 | } 15 | } 16 | catch( Exception ex ) 17 | { 18 | result = 0; 19 | return ex.HResult; 20 | } 21 | } 22 | 23 | void ITest.addManaged( ITest managed, int a, int b, out int result ) 24 | { 25 | throw new NotImplementedException(); 26 | } 27 | 28 | void ITest.testPerformance( ITest managed, out int xor, out double seconds ) 29 | { 30 | throw new NotImplementedException(); 31 | } 32 | void ITest.testStreams( Stream stmRead, Stream stmWrite ) 33 | { 34 | throw new NotImplementedException(); 35 | } 36 | void ITest.createFile( string str, out Stream stmWrite ) 37 | { 38 | stmWrite = File.Create( str ); 39 | } 40 | 41 | void IDisposable.Dispose() 42 | { 43 | } 44 | 45 | void ITest.testMarshalBack( string str, ITest managed ) 46 | { 47 | throw new NotImplementedException(); 48 | } 49 | } -------------------------------------------------------------------------------- /PortableClient/PortableClient.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net8.0 6 | false 7 | false 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /PortableClient/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace PortableClient 4 | { 5 | class Program 6 | { 7 | static void Main( string[] args ) 8 | { 9 | if( Environment.OSVersion.Platform == PlatformID.Unix ) 10 | { 11 | // Workaround the DLL search path bug on Linux: 12 | // https://github.com/dotnet/coreclr/issues/18599 13 | LinuxUtils.preloadDll( "libcomtest.so" ); 14 | } 15 | 16 | // Console.WriteLine( "Hello, world" ); 17 | Console.WriteLine( "64 bit process: {0}", Environment.Is64BitProcess ); 18 | 19 | Tests.testMarshalBack(); 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /PortableClient/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "PortableClient": { 4 | "commandName": "Project", 5 | "nativeDebugging": true 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /PortableClient/TestClass.cs: -------------------------------------------------------------------------------- 1 | using ComLight; 2 | using System; 3 | using System.IO; 4 | using System.Runtime.InteropServices; 5 | 6 | namespace PortableClient 7 | { 8 | // The generated assembly contains code like this one. 9 | static class TestProxyDelegates 10 | { 11 | [UnmanagedFunctionPointer( RuntimeClass.defaultCallingConvention )] 12 | public delegate int add( IntPtr pThis, int a, int b, [Out] out int result ); 13 | } 14 | 15 | class TestProxy: RuntimeClass, ITest 16 | { 17 | readonly TestProxyDelegates.add m_add; 18 | readonly Func m_addMarshaller; 19 | 20 | public TestProxy( IntPtr ptr, IntPtr[] vtbl, Guid id ) : 21 | base( ptr, vtbl, id ) 22 | { 23 | m_add = Marshal.GetDelegateForFunctionPointer( vtbl[ 3 ] ); 24 | } 25 | 26 | int ITest.add( int a, int b, out int result ) 27 | { 28 | return m_add( nativePointer, m_addMarshaller( a ), b, out result ); 29 | } 30 | 31 | void ITest.addManaged( ITest managed, int a, int b, out int result ) 32 | { 33 | throw new NotImplementedException(); 34 | } 35 | void ITest.testPerformance( ITest managed, out int xor, out double seconds ) 36 | { 37 | throw new NotImplementedException(); 38 | } 39 | void ITest.testStreams( Stream stmRead, Stream stmWrite ) 40 | { 41 | throw new NotImplementedException(); 42 | } 43 | void ITest.createFile( string str, out Stream stmWrite ) 44 | { 45 | throw new NotImplementedException(); 46 | } 47 | 48 | void ITest.testMarshalBack( string str, ITest managed ) 49 | { 50 | throw new NotImplementedException(); 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /PortableClient/Tests.cs: -------------------------------------------------------------------------------- 1 | using ComLight; 2 | using System; 3 | using System.Diagnostics; 4 | using System.IO; 5 | using System.Runtime.InteropServices; 6 | using System.Text; 7 | 8 | static class Tests 9 | { 10 | public const string dll = "comtest"; 11 | 12 | [DllImport( dll, PreserveSig = false )] 13 | static extern void createTest( [MarshalAs( UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof( Marshaler ) )] out ITest obj ); 14 | 15 | const int DISP_E_OVERFLOW = unchecked((int)0x8002000A); 16 | 17 | public static void test0() 18 | { 19 | ITest test = null; 20 | createTest( out test ); 21 | test.Dispose(); 22 | } 23 | 24 | public static void test1() 25 | { 26 | ITest test = null; 27 | try 28 | { 29 | createTest( out test ); 30 | int r; 31 | test.add( 1, 2, out r ); 32 | Debug.Assert( r == 3 ); 33 | Console.WriteLine( "Result: {0}", r ); 34 | 35 | test.add( int.MinValue, int.MinValue, out r ); 36 | } 37 | catch( Exception ex ) 38 | { 39 | Debug.Assert( ex.HResult == DISP_E_OVERFLOW ); 40 | Console.WriteLine( "Exception: {0}", ex.Message ); 41 | } 42 | test?.Dispose(); 43 | } 44 | 45 | public static void test2() 46 | { 47 | ITest test = null; 48 | createTest( out test ); 49 | 50 | ITest managed = new ManagedImpl(); 51 | int r; 52 | test.addManaged( managed, 1, 2, out r ); 53 | Debug.Assert( r == 3 ); 54 | Console.WriteLine( "Result: {0}", r ); 55 | } 56 | 57 | public static void test3() 58 | { 59 | int[] buffer = new int[ 1000000 ]; 60 | var rnd = new Random(); 61 | for( int i = 0; i < buffer.Length; i++ ) 62 | { 63 | buffer[ i ] = rnd.Next( 0x40000000 ); 64 | } 65 | 66 | ITest test = null; 67 | createTest( out test ); 68 | 69 | int result = 0; 70 | var sw = Stopwatch.StartNew(); 71 | for( int i = 0; i < buffer.Length; i++ ) 72 | { 73 | int r; 74 | test.add( buffer[ i ], buffer[ i ], out r ); 75 | result ^= r; 76 | } 77 | sw.Stop(); 78 | TimeSpan e = sw.Elapsed; 79 | double sec = e.TotalSeconds; 80 | Console.WriteLine( "{0:X8}", result ); 81 | 82 | double ms = sec * 1E+3; 83 | double nanosecondsPerCall = sec * ( 1E+9 / 1E+6 ); 84 | Console.WriteLine( "Managed->native interop: {0}ms for 1M calls, {1} nanoseconds / call", ms, nanosecondsPerCall ); 85 | // On my PC it says "20 nanoseconds/call", translates to 66 CPU cycles per call @ 3.3GHz stock frequency. Pretty good result, IMO. 86 | } 87 | 88 | public static void test4() 89 | { 90 | ITest test = null; 91 | createTest( out test ); 92 | ITest managed = new ManagedImpl(); 93 | int res; 94 | double sec; 95 | test.testPerformance( managed, out res, out sec ); 96 | 97 | double ms = sec * 1E+3; 98 | double nanosecondsPerCall = sec * ( 1E+9 / 1E+6 ); 99 | Console.WriteLine( "Native->managed interop: {0}ms for 1M calls, {1} nanoseconds / call", ms, nanosecondsPerCall ); 100 | } 101 | 102 | public static void testStream() 103 | { 104 | ITest test; 105 | createTest( out test ); 106 | 107 | MemoryStream ms = new MemoryStream(); 108 | using( var w = new StreamWriter( ms, Encoding.ASCII, 1024, true ) ) 109 | w.Write( "Hello, world." ); 110 | 111 | MemoryStream ws = new MemoryStream(); 112 | test.testStreams( ms, ws ); 113 | ws.Seek( 0, SeekOrigin.Begin ); 114 | 115 | using( var r = new StreamReader( ws, Encoding.ASCII ) ) 116 | { 117 | string all = r.ReadToEnd(); 118 | Console.WriteLine( all ); 119 | } 120 | } 121 | 122 | public static void testMarshalBack() 123 | { 124 | ITest test; 125 | createTest( out test ); 126 | ITest managed = new ManagedImpl(); 127 | string path = Path.Combine( Path.GetTempPath(), "test.txt" ); 128 | test.testMarshalBack( path, managed ); 129 | } 130 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This repository contains crossplatform COM interop library for .NET core. # Motivation I’m programming C++ and C#, often in the same project. Both languages are object oriented, and yet outside Windows there’s no easy way to consume objects across the native/managed boundary. When an API only has a few methods, C interop is fine. But as the API surface grows, supporting wrappers on both sides of the interop becomes time consuming and error prone. This project solves it by allowing to interop directly through OO APIs. ## Using the library [It's on nuget.org](https://www.nuget.org/packages/ComLightInterop/). The current version targets 3 platforms, .NET framework 4.7.2, .NET 8.0, and VC++, and requires Visual Studio 2022 to build.
The final version which supported .NET Core 2.1 and Visual Studio 2017 is 1.3.8. VC++ is Windows only. To build Linux shared libraries implementing or consuming COM objects, please add `build/native` directory from that package to C++ include paths. Or `ComLightLib` directory from this repository. For cmake see [include_directories](https://cmake.org/cmake/help/v3.0/command/include_directories.html) command, or use some other method, depending on your C++ build system, and compiler. 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. The library only supports `IUnknown`-based interfaces, it doesn’t handle `IDispatch`. You can only use simple types in your interfaces: primitives, structures, strings, pointers, function pointers, but not `VARIANT` or `SAFEARRAY`. `BSTR` strings probably won’t work on Linux either, I haven’t tested. ## Examples See Demos/HelloWorldCpp/HelloWorld.cpp and Demos/HelloWorldCS/HelloWorld.cs for “hello world” example. As you see, both C++ and C# sides of the interop are about 10 lines of code each. Slightly more complex example is Demos/StreamsCpp and Demos/StreamsCS, it marshals .NET streams both directions. A lot of things happening under the hood in the corresponding runtime libraries, ComLightLib for C++ and especially in ComLight for .NET. But the API is simple, at least until you want to implement custom marshallers, or do something equally advanced with the library. # Technical Details The C++ interop library is in the ComLightLib library. It implements a few template classes, providing functionality comparable to a small subset of [ATL](https://en.wikipedia.org/wiki/Active_Template_Library). When building on Linux, that library is header only, you don’t actually need to build it. On Windows there’s one .cpp file, server\freeThreadedMarshaller.cpp, required for better interop with the desktop edition of .NET runtime. On Windows, you either need to build the static library, or include that .cpp into the build system of the consuming project. If you’re using visual studio and install the nuget package, the package will reference that .cpp file from your project, so it will be compiled. A small C++ demo is implemented in NativeLibrary project. On Windows, the NativeLibrary.vcxproj project builds comtest.dll. Only tested with Visual Studio 2017. On Linux, that project builds builds libcomtest.so. It uses cmake. Only tested with gcc 8.2.0, should be portable to any C++/14 compiler. When building on Windows, my implementation is binary compatible with Microsoft’s COM. ComLightDesktop project builds a Windows console application which consumes an object from that dll using the built-in COM interop from the desktop version of .NET framework. The managed side of the interop is implemented in ComLight project. It targets .NET core 2.2. Tested on both Windows and Linux. PortableClient project builds a cross-platform test application. ## How it Works I'm using Reflection.Emit to build delegate types, marked with `[UnmanagedFunctionPointer]` attribute. The delegates have one more parameter of type `IntPtr`, for the native `this` pointer. I copy the rest of the parameters from the managed interface methods, along with their custom attributes, if any. Then I use [Marshal.GetDelegateForFunctionPointer](https://docs.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.marshal.getdelegateforfunctionpointer?view=netframework-4.8) with these delegates to expose a C++ object to .NET, or [Marshal.GetFunctionPointerForDelegate](https://docs.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.marshal.getfunctionpointerfordelegate?view=netframework-4.8) to build virtual method table wrapping a C# object for use by C++. ## Performance The interop code in ComLight assembly uses [Reflection.Emit](https://docs.microsoft.com/en-us/dotnet/api/system.reflection.emit?view=netframework-4.8) and [System.Linq.Expressions](https://docs.microsoft.com/en-us/dotnet/api/system.linq.expressions?view=netframework-4.8) to generate boilerplate code in runtime. The generated code runs pretty fast. Specifically, on my PC, both Linux and Windows versions are spending about 20 nanoseconds per C# -> C++ call, that’s about 70 CPU cycles. The other way, calling from C++ to .NET, is even faster on Windows, 15 nanoseconds per call, about 50 cycles. Strangely enough, on Linux it’s the same 20 nanoseconds both ways. For optimal performance, I recommend designing your code so the objects are relatively long-lived. If you’re going to create and destroy these managed wrappers at a rate much higher than 1 kHz, especially if these objects have methods with custom marshaled arguments (e.g. if they create or consume other COM objects), the library gonna waste noticeable CPU time compiling .NET expressions into [CIL](https://en.wikipedia.org/wiki/Common_Intermediate_Language) and then into x86/AMD64/ARM code. Not good for performance. I’ve tested on Windows 10 and Ubuntu Linux running on AMD64 CPUs, also Debian Linux running on ARM v7.1 SoC, with .NET core 2.2. Should also work on .NET Core 2.1 LTS. ## Known Issues In C++, `using namespace ComLight;` statement breaks compilation when that statement is before ComLight headers. This source code fails to compile on Windows, complaining about ambiguous `IUnknown` symbol: ``` using namespace ComLight; #include "../ComLightLib/comLightServer.h" ``` If you want to import the complete namespace as opposed to individual types, do it after you have included the headers, like this: ``` #include "../ComLightLib/comLightServer.h" using namespace ComLight; ``` --------------------------------------------------------------------------------