Willkommen zum (richtigen) vierten Teil der Tutorialserie! Hier wird es sich endlich um Delegates (Funktionszeiger) drehen, mit denen wir eine Font registrieren werden und Text zeichnen! Dabei wird auch auf die Hürde eingegangen, das manche native Funktionen nicht von C# aufgerufen werden können.
Im letzten Teil haben wir die MemoryHelper Klasse behandelt, welche auch die Funktion “GetFunctionFromAddress<T>” enthält. Anhand dieser Funktion werden wir sofort mit dem ersten Funktionszeiger anfangen, welcher eine Font (Schriftart) der Engine registriert und uns das Handle zurückliefert. Der native C++-Code sieht dazu so aus:
typedef void* (*GetFontType_)(char* FontName, int Unknown1);
Diese Funktion erwartet also einen FontName, sowie unbekannter zweiter Parameter als Integer. Zudem wird ein Handle, hier in Form eines void-Zeigers zurückgeliefert. Ein Void-Zeiger ist einfach ein Zeiger ohne einen bestimmten Datentyp wie Int oder Char.
Quelle : http://easysurfer.back2hack.cc/
[spoiler]
In C# sieht das ganz ähnlich aus. Nur verwenden wir hier kein Char-Zeiger als ersten Paramter (was zwar auch möglich wäre, aber unsafe Code sollte vermieden werden wo es möglich ist), sondern ein Buffer aus Bytes, was im Grunde auf das selbe hinausläuft.
public delegate int RegisterFontDelegate(Byte[] FontName /* ASCII */, Int32 Number); static public RegisterFontDelegate RegisterFontFunc;
Hier wird anstatt eines void-Zeigers einfach ein Int zurückgeliefert, was die Engine aber nicht weiter stört. Zeiger ist Zeiger.
In der zweiten Zeile wird eine statische Instanz vom Typ RegisterFontDelegate angelegt. Diese Funktion werden wir später aufrufen um die native Funktion auszuführen. Zudem wird dieser statische Typ auch über die GetFunctionFromAddress in folgender Weise gefüllt:
TextManager.RegisterFontFunc = MemoryHelper.GetFunctionFromAddress((IntPtr)0x50BE70);
Simple, nicht? Als Typ übergeben wir einfach das Delegate und wir erhalten die Funktion zu dem Delegate zurück! Diese können wir nun simple über folgende Weise aufrufen:
// Registers a font and returs the Handle to the font public static Int32 RegisterFont(String sFontName) { return RegisterFontFunc(Encoding.ASCII.GetBytes(sFontName), 0); } public static void Init() { dicFonts = new Dictionary(); dicColors = new Dictionary(); dicFonts.Add(Font.Smalldevfont,TextManager.RegisterFont("fonts/smalldevfont")); InitColor(Color.Blue, new Color_T(0, 0, 255, 255)); InitColor(Color.Green, new Color_T(0, 255, 0, 255)); InitColor(Color.Red, new Color_T(255, 0, 0, 255)); InitColor(Color.White, new Color_T(255, 255, 255, 255)); }
Die Funktion “RegisterFont” ruft einfach die RegisterFontFunc auf. Dabei wird der Byte[]-Buffer mit den ASCII-Zeichen des Font-Names gefüllt und als zweiter, unbekannter Parameter einfach 0 übergeben.
Die Font wird als nächstes in ein Dictionary<FontName, Int32> von Fonts eingefügt und ist ab sofort dort gespeichert.
Bevor wir jetzt in die eigentliche Zeichenfunktion gehen, erkläre ich noch schnell den unteren Teil des Sources mit den Farben. Natürlich erwartet die DrawEngineText-Funktion auch eine Farbe, und diese ist (wie man später sehen wird) als Zeiger von Color_T definiert. Color_T ist dabei einfach eine Klasse mit vier Floats der Farben RGB und Alpha. Da wir keinen Unsafe Code schreiben wollen, müssten wir für jedes Handle dieser Font unmanaged Speicher reservieren und die Struktur dorthin kopieren. Von dort aus übergeben wir einfach den Zeiger zu dieser Speicheradresse und die Engine hat nichts mehr zu meckern.
public static void InitColor(Color ColorName, Color_T Color) { IntPtr ColorPtrTemp = Marshal.AllocHGlobal(16); Marshal.StructureToPtr(Color, ColorPtrTemp,false); dicColors.Add(ColorName, (int)ColorPtrTemp); }
Es wird der Name der Farbe, sowie die Farb-Klasse übergeben. Nun Reservieren wir uns 16 Byte im Speicher (4 Floats = 4 * 4 Bytes) über Marshal.AllocHGlobal. Dann kopieren wir diese Klasse über Marshal.StructureToPtr an den reservierten Speicher und fügen das “Handle” (also den Zeiger zu der Farbe) in unsere Liste ein. Für StructureToPtr muss die Klasse übrigens exakte Layout-Informationen haben . Der dritte Parameter gibt an, ob die originale Referenz im managed Speicher gelöscht werden soll.
Im Einstiegspost hab ich bereits über diverse Komplikationen bezüglich CallingConventions gesprochen. Genau so einen Fall haben wir hier. Egal wie man es versucht hinzubiegen, das Programm stürzt immer ab. Hier mal ein Vergleich der DrawEngineText-Funktion, einmal von C++, einmal von C# aufgerufen:
C++:
ECX 0029F3D8 EDX 000011F8 EBX 00000001 ESP 000CFD64 EBP 00018000 ESI 0CFA119C EDI 00000008 EIP 00510790 iw4mp.00510790 --------------------------------------------------------------------- 000CFD70 |6C2D600D RETURN to MW2_Bot_.6C2D600D from iw4mp.00510710 000CFD74 |6C2D8C4C ASCII "TestText" 000CFD78 |7FFFFFFF ; Text Length 000CFD7C |013EB564 ; Registed Font @ iw4mp.013EB564 000CFD80 |41200000 ; Pos X 000CFD84 |41200000 ; Pos Y 000CFD88 |3F800000 ; Scale X 000CFD8C |3F800000 ; Scale Y 000CFD90 |00000000 ; Unknwon 000CFD94 |0029F3D8 ; Color-Structure 000CFD98 |00000000 ; Mode
C# Aufruf:
EAX 00000080 ECX 63836724 mscorwks.63836724 EDX 0AE0DF30 ASCII "Testtext" EBX 0022C670 ESP 000CF4E4 EBP 000CF528 ESI 000CF568 EDI 000CF880 EIP 00510790 iw4mp.00510790 000CF4E4 63661D29 RETURN to mscorwks.63661D29 ; Back to the CLRHosting 000CF4E8 0AE0DF30 ASCII "Testtext" 000CF4EC 7FFFFFFF ; Same Text length 000CF4F0 013EB564 ; (SAME) Registerd Font @ iw4mp.013EB564 000CF4F4 41200000 ; Same 000CF4F8 41200000 ; Same 000CF4FC 3F800000 ; Same 000CF500 3F800000 ; Same 000CF504 00000000 ; Same 000CF508 0029F3D8 ; SAME ColorStructure at same memory location 000CF50C 00000000 ; Same
Der Stack ist in beiden Fällen fast identisch, nur die Register haben andere Werte. Diese anderen Registerwerte kommen von dem CLRHosting, welches wohl ein paar Register verschiebt. Dagegen machen können wir nicht wirklich was, denn selbst wenn wir die Register über InlineASM setzen würden, diese wieder bei dem Funktionsaufruf überschrieben wären.
Ich bin noch am austüfteln von einer Lösung, mir kam da eine Idee mit einer CodeCave, aber das muss ich nochmal überdenken Auf jeden Fall hab ich ein Workaround, der relativ simple ist: Ich übergebe einfach den Funktionszeiger zu einer statischen C++ Funktion, welche die DrawEngineText-Funktion aufruft!
typedef int (*DrawEngineText_)(char* Text, int Unknown1, void* Font, float X, float Y, float Unknown2, float Unknown3, float Unknown4, RGBA_COLOR * Color, int Unknown5); static DrawEngineText_ DrawEngineText; static void DrawTextFuncNew(char* Text, int Font, float X, float Y, RGBA_COLOR * Color) { DrawEngineText(Text, 0x7FFFFFFF,(void*)Font,X,Y,1.0f,1.0f,0.0f,Color,0); }
Hier sehen wir wieder den selben Typedef wie oben, sowie die Funktion “DrawTextFuncNew”. Diese erwartet ein paar weniger Parameter als die originale Funktion, also nur Text, das Font-Handle, eine X und Y Position und den Zeiger zu der Farbe. Und von dieser Funktion holen wir uns den Zeiger und übergeben ihn bei der Init-Funktion an unseren C#-Code. Dazu verwende ich eine Klasse, welche einfach die Funktionszeiger speichert. Von dieser Klasse übergebe ich den Pointer und lese die darin gespeicherten Funktionszeiger aus!
class FunctionPointers { public: int iNumElements; int pElements[10]; FunctionPointers(int _iNumElements) { this->iNumElements = _iNumElements; for (int i = 0; i < 10; i++) pElements[i] = 0; } }; FunctionPointers* Pointers = new FunctionPointers(3); Pointers->pElements[0] = (int)(&DrawHelper::DrawTextFuncNew); Pointers->pElements[1] = (int)(&BoneHelper::GetPlayerTag); std::stringstream IntConvert; IntConvert << Pointers; // Ruft die Adresse von "Pointers" ab CLRHelper::InitCLR(); // castet den StringStream to einen Char-Zeiger (String) CLRHelper::ExecuteMethod("Init",(char*)IntConvert.str().c_str());
Joa, der Code macht im Grunde folgendes: Eine Instanz von “FunctionPointers” wird erstellt und diese mit zwei Funktionszeigern gefüllt. Nun wird ein StringStream erstellt und die Adresse von Pointers dort reingeschrieben. Dieses wird nun nach einem char* gecastet, dass die CLRHelper::ExecuteMethod diesen Parameter akzeptiert. Ich denke es hätte auch elegantere Methoden zum konvertieren von einem Int zu einem C-String gegeben, aber das läuft
Nun wird das ganze im C#-Code ausgelesen und der Zeiger zu der neuen DrawEngineText-Funktion gesetzt:
public delegate void DrawStringDelegate(Byte[] Text, int iFontHandle, float X, float Y, int ColorT); static public DrawStringDelegate DrawStringFunc; // In Main-Klasse List lstFunctionPointers = new List(); IntPtr PointersStructBegin = (IntPtr)Int32.Parse(sParam, NumberStyles.AllowHexSpecifier); int iCount = Marshal.ReadInt32(PointersStructBegin); for (int i = 1; i < iCount + 1; i++) { lstFunctionPointers.Add(Marshal.ReadInt32((IntPtr)(PointersStructBegin.ToInt32() + 4 * i))); } // The order of the functions also can be seen in the nativ code // 1: DrawEngineText // 2: GetTagPos GameManager.RegisterFunctions(lstFunctionPointers); // GameManager.RegisterFunctions: public static void RegisterFunctions(List lstFunctionPointers) { TextManager.DrawStringFunc = MemoryHelper.GetFunctionFromAddress((IntPtr)lstFunctionPointers[0]); TextManager.RegisterFontFunc = MemoryHelper.GetFunctionFromAddress((IntPtr)0x50BE70); }
Viel Code für wenig Arbeit… Aber so können noch folgende Funktionszeiger komfortabel hinzugefügt werden
Die DrawTextFunc() kann nun einfach in der Render-Loop aufgerufen werden um Text darzustellen!
Greez Easy
[/spoiler]