воскресенье, 19 июня 2011 г.

C++/C# interop

If you have a rooted hierarchy and have to deal with C++/C# interop without using C++/CLI, you may want to use my approach.

1. Remember that C++ pointers are tricky.

C# defines IntPtr which simply means "native integer". In C++, however, things are more complicated. For example, dynamically allocated objects are all "IntPtr", however, a class with multiple inheritance, depending on the implementation, may use different pointer offsets depending on the currently cast type. For example, if a TMyObject* object which inherits from both TObject and IInterface, is cast to IInterface*, calling TObject's methods on it from the C# side would result in undefined behavior, probably crashes. Same goes for classes with virtual functions -- underlying pointers may be different when cast to different classes in the hierarchy. Calling a method with a wrong pointer will segfault, or will invoke a wrong method, which is very difficult to debug from C#.

To circumvent this limitation, always cast objects to the root class, when exposing pointers to the C# side, Then, when you fetch a pointer, you cast it from the root class to the target class judging from the context. For example:

  1. EExceptionCode INTEROP_DECL SceneAddChild(TObject* self, TObject* ent)  
  2. {  
  3.     BARRIER_BEGIN  
  4.         ((TScene*)self)->AddChild((TEntity*)ent);  
  5.     BARRIER_END  
  6. }  

This will result in slower code because of implicit dynamic_casts (managed-to-unmanaged transition is slow anyway), on the other hand you have a lot of benefits: 1) type-safe code (across managed-unmanaged boundaries) 2) cross-compiler and cross-platform 3) less glue code (you don't need to write TParentClass_DoIt, TChildClass_DoIt, TGrandChildClass_DoIt method wrappers; TParentClass_Doit will be sufficient).

2. Don't let managed exceptions propagate through native code and vice versa.

It is usually just bad. C++ exceptions know nothing about C# exception handling mechanism (unless SEH is used, but you never know), they will skip all managed frames altogether and just crash the whole app.

Ignoring them is bad too, we need to make them communicate somehow. For the C++/glue side, I implemented two simple macros:
  1. #define BARRIER_BEGIN EExceptionCode e_code = EC_OK; try {  
  2. #define BARRIER_END } catch(TException& e) { laste = e; e_code = e.Code(); } return e_code;  

Surround any glue code with them even if you're sure it won't throw (to be sure and to have a consistent API). You have to change your signature so that it returns exception codes via the usual return mechanism, and actual objects, if any, are returned via a pointer as the last argument. laste in the macro is a thread-local variable that contains the actual exception object we have caught to be able to refer to it later from the C# side.

Then the C# side would have something similar to this:
  1. static Exception ConvertNativeExceptionToCLRException(EExceptionCode code, string msg)  
  2. {  
  3.     switch(code)  
  4.     {  
  5.         case EExceptionCode.EC_OK:  
  6.             return null;  
  7.           
  8.         case EExceptionCode.EC_NOT_IMPLEMENTED:  
  9.             return new NotImplementedException(msg);  
  10.   
  11.         case EExceptionCode.EC_INVALID_STATE:  
  12.             return new InvalidOperationException(msg);  
  13.           
  14.         case EExceptionCode.EC_OUT_OF_RANGE:  
  15.             return new ArgumentOutOfRangeException(msg);  
  16.           
  17.         case EExceptionCode.EC_FILE_NOT_FOUND:  
  18.             return new System.IO.FileNotFoundException(msg);  
  19.           
  20.         case EExceptionCode.EC_PLATFORM_DEPENDENT:  
  21.             return new PlatformDependentException(msg);  
  22.           
  23.         case EExceptionCode.EC_MALFORMED_STRING:  
  24.             return new MalformedStringException();  
  25.           
  26.         case EExceptionCode.EC_CUSTOM:  
  27.             return new CustomException();  
  28.           
  29.         case EExceptionCode.EC_ILLEGAL_ARGUMENT:  
  30.             return new ArgumentOutOfRangeException();  
  31.           
  32.         default:  
  33.             return new Exception(String.Format("Unknown exception '{0}'.", msg));
  34.     }             
  35. }  
  36.   
  37. static void ThrowFor(EExceptionCode e)  
  38. {  
  39.     string msg = "";  
  40.       
  41.     if(e != EExceptionCode.EC_OK)  
  42.     {  
  43.         IntPtr lastMsg = GetLastExceptionMessage();  
  44.         if(lastMsg != IntPtr.Zero)  
  45.             msg = Marshal.PtrToStringAnsi(lastMsg);  
  46.     }  
  47.   
  48.     Exception clrException = ConvertNativeExceptionToCLRException(e, msg);  
  49.     if(clrException != null)  
  50.         throw clrException;  
  51. }  


And then a usual P/Invoke call should have the following pattern:

  1. IntPtr canvshndl;  
  2. Native.ThrowFor(Native.CanvasCreate(out canvshndl));  

This will seamlessly integrate C++ and .NET exceptions.

Another caveat is when managed code inside a marshaled callback throws an exception while being possessed by native code. This is bad too. A managed exception knows nothing about native frames and C++ destructors, it will just skip them all until it reaches a managed frame, leaving us to memory leaks. For C#, there's no other way as to either ignore exceptions with an empty try...catch block, or just log the error, or in case you have some sort of queue, you can postpone the execution of the exception handler (probably rethrowing the exception) to a future time, outside native code.

3. Don't forget compilers are different.

Explicitly specify calling conventions. Use your own, explicit types instead of bools and enums in the glue code interface (for example, `typedef int` for both cases). Different compilers (even sometimes minor versions) implement these types differently, while C# expects something definite.

Комментариев нет:

Отправить комментарий