Geeks With Blogs
Step-by-Step into the cloud a blog of Dirk Eisenberg (>)

Wow, jetzt ist schon wieder ein ganzer Monat vergangen, ohne das ich einen Blog-Beitrag fertig bekommen habe. Ich schiebe das mal auf den Umstand das der Tag nur 24 Stunden hat und besser 48 haben sollte. Aber nun zum Thema dieses Beitrags.

Mich quälte in letzter Zeit eine Frage: Kann ich aus native C++ Code in ein Managed Assembly springen und wenn ja wie überlistet man den Garbage Collector damit man nicht mit üngültigen Pointer nach dem nächsten Cycle dasteht. Betrachten wir folgendes typische Pattern einer beliebigen Runtime wie so auch unter Windows selbst vorkommt. Um am geselligen Leben dieser Runtime teilhaben zu dürfen, wird uns die Implementierung einer DLL mit folgenden Einsprungspunkten vorgegeben (und das auch noch mit __stdcall):

  • Init( LPVOID* lpUserDefinedData );
  • Event1( LPVOID lpUserDefinedData );
  • Event2( LPVOID lpUserDefinedData );
  • UnInit( LPVOID lpUserDefinedData );

Wir haben also 4 Funktionen als Interface, eine Init, eine UnInit und zwei Aktionen. Der Entwickler der Runtime hat uns die Möglichkeit gegeben benutzerspezifische Daten durchzuschleifen um keine globalen Objekte in unserem Modul halten zu müssen, super. Jetzt stellen sich zwei Fragen:

  • Ist es möglich das gesamte Modul in .NET zu implementieren ohne die Runtime selbst zu verändern?
  • Wie schafft man es eine Referenz auf eine .NET-Klasse über den UserData-Parameter durchzuschleifen ohne das der GC bei der Heap-Reorganisation den Pointer zerstört ?

Frage 1 beantwortet sich recht einfach, .NET bietet die Möglichkeit native Funktions-Exporte zu erzeugen. Dadurch wird ein Eintrag in der EAT des PE-Files erzeugt, der direkt in den .NET-Code springt (Das ist mal einen eigene Beitrag wert). Hier ist es nun möglich mit gemanagten Objekten zu arbeiten. Etwas versteckt findet sich auch die Möglichkeit Exporte zusätzlich über ein Def-File zu steuern und nicht über die __cdeclspec(dllexport)-Anweisung.

Wie könnten jetzt also die Implementierung unsere Funktionen aussehen:

unsigned int __stdcall Init( void** lpUserData )
{
  return 0;
};

Damit können wir uns der zweiten Fragen widmen: "Wie bekomme ich jetzt eine Referenz auf ein .NET-Objekt durchgeschleift?" Den ersten Gedanken verschwenden wir an den pin-Mechnismus der doch funktionieren sollte. Folgender Code könnte das Problem lösen:

unsigned int __stdcall Init( void** lpUserData )
{
  if ( lpUserData )
  {
     CMyManagedObject^ refOb = gcnew CMyManagedObject();
     pin_ptr pInteriorPointer = &refOb;
     *lpUserData = pInteriorPointer;
  }
  return 0;
};

Hierzu muss man wissen das gepinnte Pointer nur so lange gültig sind, bis das entsprechende StackFrame verlassen wird. Dadurch wäre unser Pointer schon bald nicht mehr sicher und der GC würde beim nächsten Cycle unter Umständen den Heap verändern. Wie also weitermachen? Mit GC-Handles steht uns eine Möglichkeit zur Verfügung einen eindeutigen und positionsunabhängigen Verweis auf ein managed Objekt zu erzeugen. Als besonders nützlich stellt sich die Tatsache heraus, das dieser Verweis in Form eines Integer Wertes daher kommt. Also auf gehts:

if ( lpUserData )
{
  CMyManagedObject^ refOb = gcnew CMyManagedObject();
  GCHandle gch = GCHandle::Alloc( refOb );
  IntPtr iRef = gch.ToIntPtr( gch );
  *lpUserData = (void*)(int)iRef;
 }

Schon fast fertig. Der Integer kann jetzt jederzeit in das Objekt zurück verwandelt werden, solange nicht GCHandle::Free aufgerufen wird. Diese Methode muss aber aufgerufen werden, sobald dieses Handle nicht mehr benötigt wird. Jetzt kommt uns aber ein entscheidender Vorteil zu Hilfe. Im Gegensatz zu pin_ptr darf ein GCHandle-Objekt Member einer Klasse sein. Dadurch ist es möglich die ganze Schnittstelle in einer .NET-Klasse zu kapseln:

class ref CMyManagedObject
{
  public:
    IntPtr GetFixedId()
    {
       if (!m_gch.IsAllocated())
         m_gch = GCHandle::Alloc( this );
       return GCHandle::ToIntPtr( m_gch );
    }

    void ReleaseHandle()
    {
      m_gch.Free();
    }

    static CMyManagedObject^ GetTarget( IntPtr ip )
    {
      GCHandle gcHandle = GCHandle::FromIntPtr( ip );
      return (CMyManagedObject^)gcHandle.Target();
    }

  protected:
    GCHandle m_gch;
}

Jetzt habe wir die Klasse, nur wie sieht unser Implementierung aus:

unsigned int __stdcall Init( void** lpUserData )
{
  if ( lpUserData )
  {
     CMyManagedObject^ refOb = gcnew CMyManagedObject();
     IntPtr ip = refOb->GetFixedId();
     *lpUserData = (void*)(int)ip;
  }
  return 0;
};

unsigned int __stdcall UnInit( void* lpUserData )
{
  if ( lpUserData )
  {
     CMyManagedObject^ refOb = CMyManagedObject::GetTarget( (IntPtr)lpUserData )
     refOb->ReleaseHandle();
  }
  return 0;
};

Ruft das Framework jetzt die Einsprungspunkte auf, haben wir jederzeit die Möglichkeit auf das .NET-Objekt zu verweisen und den gesamten Umfang des .NET-Frameworks zu nutzen. Durch diese Technik ist es Möglich bestehende Frameworks um Erweiterungen zu bereichern, die mit Hilfe des .NET-Frameworks entwickelt wurden.

Posted on Friday, November 11, 2005 8:26 PM .NET Coding | Back to top


Comments on this post: Interior Pointer und GCHandles

# re: Interior Pointer und GCHandles
Requesting Gravatar...
Wäre folgendes Snippet native C/C++ und ip ein Pointer, dann würde der /Wp64 Compilerschalter sofort anspringen, denn der cast zu int würde unter x64 sofort das obere DWORD des Pointers ausradieren:

*lpUserData = (void*)(int)ip;

Was passiert hier mit native x64 Code und einer dazu passenden Dotnet-Runtime? Alles null-Problemo?


Left by SKU on Nov 12, 2005 9:53 AM

# re: Interior Pointer und GCHandles
Requesting Gravatar...
Nicht ganz der Cast(int) in .NET wird zu einem Int64 unter native x64 code (denke ich, in Ermangelung einer 64Bit-Maschine). Der Pointer ist ja auch 64Bit breit und alles sollte gehen. Im schlimmsten Fall muss man die Casts mit den entsprechenden plattformspezifischen Typen machen (IntPtr::ToInt32 bzw IntPtr::ToInt64 ) und so das Problem umgehen.
Left by dirk on Nov 12, 2005 11:38 AM

Your comment:
 (will show your gravatar)


Copyright © Dirk Eisenberg | Powered by: GeeksWithBlogs.net