Event from external callback - c#

I'm creating a project of data transmitter using custom DLL written in ANSI C. My aim is to send chunks of data, which each of them must be confirmed after sending by build in OnDataSendingDone callback.
The problem is that I have no idea how to hook that callback to an event, so I can wait for it after each SendByte. Maybe event is not such a good idea.
My question is: how to wait for the OnDataSendingDone callback after SendByte?
Please give me some clues. Here is the code snippet:
class LibWrapper
{
[DllImport(dllPath, CallingConvention = CallingConvention.Cdecl, EntryPoint = "SetCallback")]
public static extern bool SetCallback(byte functype, Delegate func);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate void CallbackDelegate(UInt32 handle);
private static CallbackDelegate OnDataSendingDone;
public void OnDataSendingDoneCallback(CallbackDelegate callback)
{
OnDataSendingDone_ft = new CallbackDelegate2(callback);
SetCallback(FT_DATASENDINGDONE, OnDataSendingDone_ft);
}
}
class Transmitter
{
LibWrapper lib = new LibWrapper();
byte[] data = new byte[10];
public void OnDataSendingDone(UInt32 handle)
{
return;
}
lib.OnDataSendingDoneCallback(OnDataSendingDone);
public void TransmitData()
{
// here: sequential data transmission
foreach (byte b in data)
{
lib.SendByte(b);
// here: wait for OnDataSendingDone
}
}
}

If I understand correctly, you could do something like this:
class Transmitter
{
LibWrapper lib = new LibWrapper();
private AutoResetEvent evt = new AutoResetEvent(false);
byte[] data;
public void OnDataSendingDone(UInt32 handle)
{
evt.Set();
}
public void TransmitData()
{
// here: sequential data transmission
foreach (byte b in data)
{
lib.SendByte(b);
if (!evt.WaitOne(15000)) // or whatever timeout makes sense
throw new Exception("timed out");
}
}
}

Related

Pass delegate as argument to a method from one library to another library and invoke it from receiving library

I wanted to pass a delegate from one library to another library.
For ex:
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate void CallbackDelegate();
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate int RegisterCallback(IntPtr libPtr, CallbackDelegate func);
protected RegisterCallback registerCallBackDelegate;
public class Class
{
public bool Register()
{
CallbackDelegate callback = new CallbackDelegate(OnEvent);
int result = registerCallBackDelegate(context, callback);
}
public void OnEvent()
{
Console.WriteLine("event received");
}
In the above example callback delegate is sent as an arg to another library which is also written in c# like below
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate void CallbackDelegate();
private static IntPtr callBackPtr;
[UnmanagedCallersOnly(EntryPoint = "RegisterCallback", CallConvs = new[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })]
public static int RegisterCallback(IntPtr context, IntPtr callback)
{
callBackPtr = callback;
return 0;
}
private static void OnExternalEvent(object sender, FileSystemEventArgs e)
{
var callbackDelegate = (CallbackDelegate)Marshal.GetDelegateForFunctionPointer(callBackPtr, typeof(CallbackDelegate));
callbackDelegate();
}
Now the issue is on the above method. Getting the bellow error.
'[A]CallbackDelegate cannot be cast to [B]CallbackDelegate. Type A originates from Lib1. Type B originates from Lib2.
Any idea how to convert this IntPtr to delegate and call it without this cast issue?

Unity C++ DLL crash when calling handler passed from Unity to C# wrapper to Native lib

I have found a C++ lib https://github.com/yhirose/cpp-httplib and I am trying to write a C# wrapper around it to be able to use it from Unity.
To bind handlers for POST / GET routes you use;
server->Post("my/route", [](const httplib::Request& req, httplib::Response& res) {
// do logic here
})
I have created a C++ project, and wrote some of my types there which will be used as intermediary types since I don't need all the data. Then I wrote my UServer class which manages the httplib::Server class and there I have
void UServer::Post(const char* pattern, RequestHandler handler)
{
m_Server->Post(pattern, [=](const httplib::Request& req, httplib::Response& res)
{
userver::Request* u_req = userver::request_to_urequest(req);
userver::Response* u_res = userver::response_to_uresponse(res);
(*handler)(u_req, u_res);
// Free memory
safe_delete(u_req);
safe_delete(u_res);
});
}
Next, I have created helper methods and exposed them with
extern "C" {
USERVER_API UServer* UServer_New(const char* hostname, int port,
const char* cert, const char* key)
{
return new UServer(hostname, port, cert, key);
}
USERVER_API void UServer_Delete(UServer* server)
{
delete server;
}
USERVER_API void UServer_Start(UServer* server)
{
server->Start();
}
USERVER_API void UServer_Stop(UServer* server)
{
server->Stop();
}
USERVER_API void UServer_Post(UServer* server, const char* route, RequestHandler handler)
{
server->Post(route, handler);
}
}
Now in C#, I wrote a wrapper class for that class
public sealed class Server : IDisposable
{
private delegate void ServerRequestHandlerNative(IntPtr reqPtr, IntPtr resPtr);
public delegate void ServerRequestHandler(Request req, Response res);
[DllImport("UServer", EntryPoint = "UServer_New")]
private static extern IntPtr UServer_New(string hostname, int port, string cert, string key);
[DllImport("UServer", EntryPoint = "UServer_Delete")]
private static extern void UServer_Delete(IntPtr server);
[DllImport("UServer", EntryPoint = "UServer_Start")]
private static extern void UServer_Start(IntPtr server);
[DllImport("UServer", EntryPoint = "UServer_Stop")]
private static extern void UServer_Stop(IntPtr server);
[DllImport("UServer", EntryPoint = "UServer_Post")]
private static extern void UServer_Post(IntPtr server, string route, IntPtr handler);
[DllImport("UServer", EntryPoint = "UServer_Get")]
private static extern void UServer_Get(IntPtr server, string route, IntPtr handler);
public void Dispose()
{
Console.WriteLine("[C#] Called dispose.");
UServer_Delete(m_ServerPtr);
}
private IntPtr m_ServerPtr;
public Server(string hostname, int port, string cert, string key)
{
m_ServerPtr = UServer_New(hostname, port, cert, key);
}
public void Start()
{
UServer_Start(m_ServerPtr);
}
public void Stop()
{
UServer_Stop(m_ServerPtr);
}
public void Post(string route, ServerRequestHandler handler)
{
ServerRequestHandlerNative nativeHandler = delegate (IntPtr reqPtr, IntPtr resPtr)
{
RequestNative reqNat = Marshal.PtrToStructure<RequestNative>(reqPtr);
ResponseNative resNat = Marshal.PtrToStructure<ResponseNative>(resPtr);
Request req = UServerUtils.NativeRequestToRequest(reqNat);
Response res = UServerUtils.NativeResponseToResponse(resNat);
Console.WriteLine("[C#] GET Successfully marshaled data. Now calling handler.");
handler(req, res);
};
UServer_Post(m_ServerPtr, route, Marshal.GetFunctionPointerForDelegate(nativeHandler));
}
and finally I am using it in Unity from Start() Mono's method
_serverTask = Task.Run(() =>
{
string hostname = "localhost";
int port = 6060;
string cert = $"{Application.streamingAssetsPath}/server.crt";
string key = $"{Application.streamingAssetsPath}/server.key";
ImGuiLogger.Instance.Log("CertPath = " + cert);
ImGuiLogger.Instance.Log("KeyPath = " + key);
_server = new UServerNET.Server(hostname, port, cert, key);
_server.Post("/THLinkAPI/ConnectionChanged", THLinkApiController.PostConnectionChangedUServer);
_server.Post("/THLinkAPI/PushEvent", THLinkApiController.PostPushEventUServer);
_server.Start();
MainThreadDispatcher.Instance.Enqueue(() =>
{
ImGuiLogger.Instance.Log("UServer started on " + hostname + ":" + port);
});
}, _tokenSrc.Token);
The problem is, only the last bound Post handler works, if I try to send request that triggers any of the before it will crash in C++ DLL at this point, when actually calling the delegate
(*handler)(u_req, u_res);
So in the sample I provided, if I send request to /THLinkAPI/PushEvent it works fine, but if I send it to THLinkAPI/ConnectionChanged it crashes.
Okay, it seems that after getting some rest new ideas came to my mind and I've figured it out.
The issue was that I was passing a delegate from C# that was then referenced from C++ as a pointer, but no reference to it remained in C# which is why it got cleaned up by GC and when later I tried to deref and call the function from C++ it pointed to garbage memory. I solved the issue by modifying the code to save the delegates to a list:
private List<ServerRequestHandlerNative> _reqHandlers = new List<ServerRequestHandlerNative>();
public void Post(string route, ServerRequestHandler handler)
{
ServerRequestHandlerNative nativeHandler = delegate (IntPtr reqPtr, IntPtr resPtr)
{
// Save the handler
ServerRequestHandler localHandler = handler;
RequestNative reqNat = Marshal.PtrToStructure<RequestNative>(reqPtr);
ResponseNative resNat = Marshal.PtrToStructure<ResponseNative>(resPtr);
Request req = UServerUtils.NativeRequestToRequest(reqNat);
Response res = UServerUtils.NativeResponseToResponse(resNat);
Console.WriteLine("[C#] GET Successfully marshaled data. Now calling handler.");
localHandler(req, res);
};
// Save the handler to keep the ref count
_reqHandlers.Add(nativeHandler);
UServer_Post(m_ServerPtr, route, Marshal.GetFunctionPointerForDelegate(nativeHandler));
}

Raise events from C to C# (iOS to Unity)

I would like to register for events in Unity and raise those events from C in iOS.
I have the following pattern -
// Unity side register delegate and event handler
private delegate void CallbackDelegate(CBObj data);
public static event CallbackDelegate dataUpdatedEvent;
// for iOS
#if !UNITY_EDITOR && UNITY_IOS
[DllImport("__Internal")]
private static extern void PluginFunction(CallbackDelegate callback);
#endif
public CBObj {
// marshal C objects to c# objects in the constructor here using the Ptr from C
}
[MonoPInvokeCallback(typeof(CallbackDelegate))]
static void CallbackMethod(CBObj dataPtr)
{
if (dataUpdatedEvent != null)
{
CBObj obj = new CBObj(dataPtr);
dataUpdatedEvent(obj);
}
}
// Unity Usage
private CallbackDelegate evt;
void Start(){
evt += updateEvent;
}
public void updateEvent(CBObj data){
// do something with data everytime its called
}
// C code
extern "C" typedef void (*CallBackFuncP) (CBObj dataPtr);
typedef struct
{
float *data1;
int *data2;
} CBObj;
extern "C" {
CallBackFuncP* cb;
void PluginFunction(CallBackFuncP callback) {
// store the the callback function pointer
cb = callback;
}
}
// raise the event somewhere in code
if (cb != NULL) {
CBObj *test = [[CBObj alloc] init];
cb(test)
}
Would this work? Is this the correct pattern? Is there a better way to do it?
Any pointers to do this are highly appreciated.
Unity suggests using UnitySendMessage function to call a C# method from C.
You can use json to format the data
NSDictionary *d = #{ #"data1":1, #"data2":2.0 };
NSData *j = [NSJSONSerialization dataWithJSONObject:d options:0 error:NULL];
NSString *s = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
UnitySendMessage("GameObjectName", "MethodName", [s UTF8String]);
In unity, create a GameObject named GameObjectName, attach a script with a method named MethodName. Use a json library to decode the string into an object.
public class YourScriptName : Monobehaviour
{
public class CBObj
{
public int data1;
public float data2;
}
public void MethodName(string data)
{
var obj = UnityEngine.JsonUtility.FromJson<CBObj>(data);
}
}
I solved it by the following patter and it worked flawlessly. Hope this helps someone.
// Unity side register delegate and event handler
delegate void Internal_CallbackDelegate(IntPtr dataPtr);
public delegate void CallbackDelegate(CBObj data);
// The above pattern is to deal with pointer conversion from iOS to Unity
public static event CallbackDelegate DataUpdatedEvent;
// YourUnityKlass for iOS
#if !UNITY_EDITOR && UNITY_IOS
[DllImport("__Internal")]
private static extern void PluginFunction(Internal_CallbackDelegate callback);
#endif
// Where this class is declared call in constructor
#if !UNITY_EDITOR && UNITY_IOS
PluginFunction(CallbackMethod);
#endif
public CBObj {
// marshal C objects to c# objects in the constructor here using the Ptr from C
private IntPtr m_Ptr;
internal CBObj(IntPtr ptr)
{
if (ptr == IntPtr.Zero)
throw new ArgumentException("ptr may not be IntPtr.Zero");
m_Ptr = ptr;
// getDataFromC();
}
// get individual pointers by exposing them via C and mashall copy the data as per your needs
}
[MonoPInvokeCallback(typeof(Internal_CallbackDelegate))]
static void CallbackMethod(IntPtr dataPtr)
{
if (DataUpdatedEvent != null)
{
CBObj obj = new CBObj(dataPtr);
DataUpdatedEvent(obj);
}
}
// Unity Usage
void Start(){
YourUnityKlass klass = new YourUnityKlass();
YourUnityKlass.DataUpdatedEvent += updateEvent;
}
public void updateEvent(CBObj data){
// do something with data every time its called
}
// C header - code
typedef struct
{
float *data1;
int *data2;
} CBObj;
typedef void (*CallBackFuncP) (CBObj* dataPtr);
#interface CallbackWrapper : NSObject
{
#public
CallBackFuncP _dataUpdate;
}
- (void) sendDataUpdate:(CBObj*) obj;
#end
// C - impl code
#implementation CallbackWrapper
-(void) sendDataUpdate:(CBObj*) objPtr
{
if (_dataUpdate != NULL){
_dataUpdate(objPtr);
}
}
#end
extern "C" {
void PluginFunction(CallBackFuncP callback) {
// Create a swiftKlass to store your callback pointer
CallbackWrapper* cb = [[SwiftKlass shared] getCallback];
cb->_dataUpdate = callback;
}
}
// Swift usage - Inside say SwiftKlass
#objc public func getCallback() -> CallbackWrapper {
return self.cb
}

NullReferenceException on unmanaged c++ code

I have a C++ DLL that I need to use in may c# project.
Here is the important part of my code:
public static class MTSCRA_API
{
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
public delegate void DataReceiveDelegate([MarshalAsAttribute(UnmanagedType.LPStr)]String x);
//More methods....
[DllImport("MTSCRA.dll", CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Auto, SetLastError = true)]
public static extern void OnDeviceConnectionStateChanged(IntPtr lpFuncNotify);
}
Where I use it:
public void Open()
{
if (!MTSCRA_API.IsDeviceConnected())
{
UInt32 result = MTSCRA_API.OpenDevice("");
if (result == 0)
{
MTSCRA_API.OnDataReceived(
Marshal.GetFunctionPointerForDelegate(
new Kiosk.Hardware.CardReaderMagTek.MTSCRA_API.DataReceiveDelegate(CardReaderMagTek_OnCardDataReceived)));
}
}
}
Mutex mutex = new Mutex();
void CardReaderMagTek_OnCardDataReceived(String info)
{
try
{
//Do stuff
}
catch(Exception ex)
{
}
finally
{
mutex.ReleaseMutex();
}
MTSCRA_API.ClearCardData();
info = null;
}
Each time I swipe a card in a device, the CardReaderMagTek_OnCardDataReceived() event is called.
The Open() method is executed and the event CardReaderMagTek_OnCardDataReceived() is called but only 9 times. A the 10º the code crash with a NullReferenceException without entering in the event and I don't have access to the callstack...
Anyone knows what could be the problem?
MTSCRA_API.OnDataReceived(
Marshal.GetFunctionPointerForDelegate(
new Kiosk.Hardware.CardReaderMagTek.MTSCRA_API.DataReceiveDelegate(
CardReaderMagTek_OnCardDataReceived)
)
);
You are not keeping your delegate alive. You create an instance of DataReceiveDelegate and pass it to GetFunctionPointerForDelegate. But after GetFunctionPointerForDelegate returns, there's no reason for the delegate to stay alive. At some point it will be collected.
Hold the delegate in a managed variable for as long as the unmanaged function needs to be able to call it.

How to make make a .NET COM object apartment-threaded?

.NET objects are free-threaded by default. If marshaled to another thread via COM, they always get marshaled to themselves, regardless of whether the creator thread was STA or not, and regardless of their ThreadingModel registry value. I suspect, they aggregate the Free Threaded Marshaler (more details about COM threading could be found here).
I want to make my .NET COM object use the standard COM marshaller proxy when marshaled to another thread. The problem:
using System;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Threading;
namespace ConsoleApplication
{
class Program
{
static void Main(string[] args)
{
var apt1 = new WpfApartment();
var apt2 = new WpfApartment();
apt1.Invoke(() =>
{
var comObj = new ComObject();
comObj.Test();
IntPtr pStm;
NativeMethods.CoMarshalInterThreadInterfaceInStream(NativeMethods.IID_IUnknown, comObj, out pStm);
apt2.Invoke(() =>
{
object unk;
NativeMethods.CoGetInterfaceAndReleaseStream(pStm, NativeMethods.IID_IUnknown, out unk);
Console.WriteLine(new { equal = Object.ReferenceEquals(comObj, unk) });
var marshaledComObj = (IComObject)unk;
marshaledComObj.Test();
});
});
Console.ReadLine();
}
}
// ComObject
[ComVisible(true)]
[Guid("00020400-0000-0000-C000-000000000046")] // IID_IDispatch
[InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
public interface IComObject
{
void Test();
}
[ComVisible(true)]
[ClassInterface(ClassInterfaceType.None)]
[ComDefaultInterface(typeof(IComObject))]
public class ComObject : IComObject
{
// IComObject methods
public void Test()
{
Console.WriteLine(new { Environment.CurrentManagedThreadId });
}
}
// WpfApartment - a WPF Dispatcher Thread
internal class WpfApartment : IDisposable
{
Thread _thread; // the STA thread
public System.Threading.Tasks.TaskScheduler TaskScheduler { get; private set; }
public WpfApartment()
{
var tcs = new TaskCompletionSource<System.Threading.Tasks.TaskScheduler>();
// start the STA thread with WPF Dispatcher
_thread = new Thread(_ =>
{
NativeMethods.OleInitialize(IntPtr.Zero);
try
{
// post a callback to get the TaskScheduler
Dispatcher.CurrentDispatcher.InvokeAsync(
() => tcs.SetResult(System.Threading.Tasks.TaskScheduler.FromCurrentSynchronizationContext()),
DispatcherPriority.ApplicationIdle);
// run the WPF Dispatcher message loop
Dispatcher.Run();
}
finally
{
NativeMethods.OleUninitialize();
}
});
_thread.SetApartmentState(ApartmentState.STA);
_thread.IsBackground = true;
_thread.Start();
this.TaskScheduler = tcs.Task.Result;
}
// shutdown the STA thread
public void Dispose()
{
if (_thread != null && _thread.IsAlive)
{
InvokeAsync(() => System.Windows.Threading.Dispatcher.ExitAllFrames());
_thread.Join();
_thread = null;
}
}
// Task.Factory.StartNew wrappers
public Task InvokeAsync(Action action)
{
return Task.Factory.StartNew(action,
CancellationToken.None, TaskCreationOptions.None, this.TaskScheduler);
}
public void Invoke(Action action)
{
InvokeAsync(action).Wait();
}
}
public static class NativeMethods
{
public static readonly Guid IID_IUnknown = new Guid("00000000-0000-0000-C000-000000000046");
public static readonly Guid IID_IDispatch = new Guid("00020400-0000-0000-C000-000000000046");
[DllImport("ole32.dll", PreserveSig = false)]
public static extern void CoMarshalInterThreadInterfaceInStream(
[In, MarshalAs(UnmanagedType.LPStruct)] Guid riid,
[MarshalAs(UnmanagedType.IUnknown)] object pUnk,
out IntPtr ppStm);
[DllImport("ole32.dll", PreserveSig = false)]
public static extern void CoGetInterfaceAndReleaseStream(
IntPtr pStm,
[In, MarshalAs(UnmanagedType.LPStruct)] Guid riid,
[MarshalAs(UnmanagedType.IUnknown)] out object ppv);
[DllImport("ole32.dll", PreserveSig = false)]
public static extern void OleInitialize(IntPtr pvReserved);
[DllImport("ole32.dll", PreserveSig = true)]
public static extern void OleUninitialize();
}
}
Output:
{ CurrentManagedThreadId = 11 }
{ equal = True }
{ CurrentManagedThreadId = 12 }
Note I use CoMarshalInterThreadInterfaceInStream/CoGetInterfaceAndReleaseStream to marshal ComObject from one STA thread to another. I want both Test() calls to be invoked on the same original thread, e.g. 11, as it would have been the case with a typical STA COM object implemented in C++.
One possible solution is to disable IMarshal interface on the .NET COM object:
[ComVisible(true)]
[ClassInterface(ClassInterfaceType.None)]
[ComDefaultInterface(typeof(IComObject))]
public class ComObject : IComObject, ICustomQueryInterface
{
// IComObject methods
public void Test()
{
Console.WriteLine(new { Environment.CurrentManagedThreadId });
}
public static readonly Guid IID_IMarshal = new Guid("00000003-0000-0000-C000-000000000046");
public CustomQueryInterfaceResult GetInterface(ref Guid iid, out IntPtr ppv)
{
ppv = IntPtr.Zero;
if (iid == IID_IMarshal)
{
return CustomQueryInterfaceResult.Failed;
}
return CustomQueryInterfaceResult.NotHandled;
}
}
Output (as desired):
{ CurrentManagedThreadId = 11 }
{ equal = False }
{ CurrentManagedThreadId = 11 }
This works, but it feels like an implementation-specific hack. Is there a more decent way to get this done, like some special interop attribute I might have overlooked? Note that in real life ComObject is used (and gets marshaled) by a legacy unmanaged application.
You can inherit from StandardOleMarshalObject or ServicedComponent for that effect:
Managed objects that are exposed to COM behave as if they had aggregated the free-threaded marshaler. In other words, they can be called from any COM apartment in a free-threaded manner. The only managed objects that do not exhibit this free-threaded behavior are those objects that derive from ServicedComponent or StandardOleMarshalObject.
Paulo Madeira's excellent answer provides a great solution for when the managed class being exposed to COM can be derived from StandardOleMarshalObject.
It got me thinking though, how to deal with the cases when there is already a base class, like say System.Windows.Forms.Control, which doesn't have StandardOleMarshalObject in its inheritance chain?
It turns out, it's possible to aggregate the Standard COM Marshaler. Similar to the Free Threaded Marshaler's CoCreateFreeThreadedMarshaler, there is an API for that: CoGetStdMarshalEx. Here's how it can be done:
[ComVisible(true)]
[ClassInterface(ClassInterfaceType.None)]
[ComDefaultInterface(typeof(IComObject))]
public class ComObject : IComObject, ICustomQueryInterface
{
IntPtr _unkMarshal;
public ComObject()
{
NativeMethods.CoGetStdMarshalEx(this, NativeMethods.SMEXF_SERVER, out _unkMarshal);
}
~ComObject()
{
if (_unkMarshal != IntPtr.Zero)
{
Marshal.Release(_unkMarshal);
_unkMarshal = IntPtr.Zero;
}
}
// IComObject methods
public void Test()
{
Console.WriteLine(new { Environment.CurrentManagedThreadId });
}
// ICustomQueryInterface
public CustomQueryInterfaceResult GetInterface(ref Guid iid, out IntPtr ppv)
{
ppv = IntPtr.Zero;
if (iid == NativeMethods.IID_IMarshal)
{
if (Marshal.QueryInterface(_unkMarshal, ref NativeMethods.IID_IMarshal, out ppv) != 0)
return CustomQueryInterfaceResult.Failed;
return CustomQueryInterfaceResult.Handled;
}
return CustomQueryInterfaceResult.NotHandled;
}
static class NativeMethods
{
public static Guid IID_IMarshal = new Guid("00000003-0000-0000-C000-000000000046");
public const UInt32 SMEXF_SERVER = 1;
[DllImport("ole32.dll", PreserveSig = false)]
public static extern void CoGetStdMarshalEx(
[MarshalAs(UnmanagedType.IUnknown)] object pUnkOuter,
UInt32 smexflags,
out IntPtr ppUnkInner);
}
}

Categories

Resources