Comments
Description
Transcript
Mixed code: C++/CLI
Mixed code: C++/CLI Raffaele Rialdi Visual Developer Security MVP [email protected] MVP Profile: http://snipurl.com/f0cv http://mvp.support.microsoft.com Agenda • Perché usare C++ ... perché C++/CLI • Carrellata sul linguaggio – – – – solo gli elementi del linguaggio utili per interop gestione della memoria managed/unmanaged pinning e marshalling mixed types • C++ future directions Perché usare C++ • Rende semplice usare codice unmanaged – Le definizioni PInvoke non sono sempre semplici da scrivere – VC++ usa IJW (It Just Works) per usare codice nativo e codice managed allo stesso tempo – Ad esempio si semplifica l'accesso alle librerie DirectX – VC++ può creare dll/exe misti con codice C# / VB / C++ • Rende semplice usare codice managed – – – – System.Xml semplifica la manipolazione di Xml System.Net semplifica l'accesso ai socket e al web System.Text.RegularExpressions semplifica le regex Idem per altre namespace/classi del framework Perché un nuovo linguaggio? Perché C++/CLI? • Le managed extensions erano complesse per aderire alle regole dello standard ISO (doppio underscore, etc.) – cl /clr:oldSyntax continua a compilare le managed extensions – un tool di Stan Lippman permette una migrazione quasi automatica • C++/CLI è vicino alla standardizzazione ECMA (in questi giorni) ed ISO (probabilmente chiamata ISO C++09) • C++/CLI ha una piacevole sintassi per lavorare sia con il mondo managed che unmanaged C++ Features •Finalizzazione deterministica •Template e generics •Uso dei tipi nativi •Multiple inheritance (unmanaged) •STL, algoritmi generici •Distinzione Puntatore/Puntato •Copy construction, assignment CLR Features •Garbage collector, finalizzatori •Generics •Reference e Value type •Interfacce •Verificabilità •Security •Proprietà, delegati, eventi Novità in C++/CLI di cui non parleremo • • • • • • • • • • • • • • • • • • trivial properties: property String ^Name; indexed properties managed copy constructors delegate + event managed operator overloading boxing/unboxing safe_cast<> generics vs templates method overriding lock(...) jagged arrays STL.NET integrazione MFC / Winform integrazione Avalon compilazione parallela profile guided optimization OpenMP parallelism CLR Delay Loading Veloce carrellata su C++/CLI • Supporto distinto per tipi managed e unmanaged C++/CLI C# public ref class MyClass { ... } ; public ref struct MyClass { ... } ; MyClass ^c = gcnew MyClass(); public class MyClass { ... } public value class MyValueType { ... } ; public value struct MyValueType { ... } ; MyValueType v; public struct MyValueType { ... } public interface class MyInterface { ... } ; public interface struct MyInterface { ... } ; public interface MyInterface { ... } public enum class MyEnum : char { ... } ; public enum struct MyEnum : char { ... } ; public enum MyEnum : char { ... } array<int> ^nums = {1,2,3} ; int[] nums = new int[] {1,2,3} MyClass c = new MyClass(); MyValueType v; Le specifiche si trovano qui: http://msdn.microsoft.com/visualc/homepageheadlines/ecma/default.aspx I nuovi operatori ^ e % • Introdotto nel linguaggio l' "handle" – – – – – – una sorta di puntatore managed ad un oggetto nel managed heap Il CLR tiene aggiornato il suo valore quando esegue la GC analogo del reference di C#, ma il reference in C++ esisteva già il simbolo è "hat" ^ su un handle si applicano gli operatori -> e * L'analogo di void* è Object^ • Nuovo allocatore di memoria managed gcnew – String ^s1 = gcnew String; – String s2; continua ad essere un espressione valida • Introdotto nel linguaggio il "tracking reference" – Analogo del reference & di C++ classico, cioè un alias all'oggetto – il simbolo è "%" Distruzione deterministica • C++/CLI introduce la distruzione deterministica delle risorse – Non deve e non può riguardare la memoria, ma solo le risorse unmanaged. Questo è lo scopo del pattern Dispose. – In pratica il Pattern Dispose viene implementato dal compilatore • Implementazione completa di GC.SuppressFinalize • Quando nella classe esiste il distruttore: – In sostanza il distruttore della classe viene mappato su Dispose – La classe implementa automaticamente IDisposable – L'uscita dallo scope o una delete esplicita provoca la chiamata a Dispose (analogo dello statement using di C#, ma più semplice) • Introdotto anche la sintassi per il finalizzatore – La sintassi è analoga al distruttore !NomeClasse() {...} – Nel finalizzatore si mette la distruzione delle risorse – Nella Dispose si mette la chiamata al finalizzatore Istruire il precompilatore • Il compilatore VC++ accetta di mixare codice managed e unmanaged anche nello stesso listato – Alcune volte potrebbe esserci ambiguità su come compilare il codice • Si può informare il compilatore con due #pragma – #pragma managed – #pragma unmanaged #pragma managed class Managed {...} ; #pragma unmanaged class Native {...} ; #pragma managed ... Memoria: Interior Pointers interior_ptr<type> name = &value; • Al contrario dell'handle, permette l'aritmetica dei puntatori • Utile per la veloce manipolazione di array e buffer • Trasparente: è usabile anche per tipi unmanaged (restituisce un puntatore classico) Esempio 1 Esempio 2 array<int>^a = {1,2,3,4,5}; interior_ptr<int> ip = &a[0]; for(int i = 0; i<a->Length; i++) Console::WriteLine(++ip[i]); String ^str1 = "Hello, world"; String ^str2 = str1; interior_ptr<String^> ip = &str1; *ip = "Ciao"; Console::WriteLine(str1 + " - " + str2); // output: 2, 3, 4, 5, 6 // output: Ciao – Hello, world Memoria: Pinning Pointers pin_ptr<type> name = &value; void F(int* p); // Func unmanaged array<int>^ arr = …; pin_ptr<int> pi = &arr[0]; F(pi); // ptr unmanaged Interior Pointer interior_ptr<T> String ^str1 = "Hello, world"; // interior pointer al buffer della stringa (non è una copia) interior_ptr<const wchar_t> ip = PtrToStringChars(str1); Pinning Pointer pin_ptr<T> // interior pointer senza 'const' interior_ptr<wchar_t> ip2 = const_cast<interior_ptr<wchar_t> >(ip); // pinning pointer Il GC non può muovere il buffer pin_ptr<wchar_t> pp = ip2; Unmanaged Pointer T* // modifico il buffer for(int i=0; i<str1->Length; i++) ++pp[i]; // caratteri ascii incrementati Console::WriteLine(str1); // out Ifmmp-!xpsme Sguardo molto semplicistico in memoria Unmanaged heap MyClass1 *pc = new MyClass1(0x30); // pc è un "puntatore" MyClass1 &rc = *pc; gg.hh.jj.kk // rc è un "reference" alias 30.00.00.00 MyClass1 *pc2 = pc; gg.hh.jj.kk MyClass2 ^hc = gcnew MyClass2(); // hc è un handle. ^ si pronuncia hat interior_ptr<MyClass2 ^> ip = &hc; xx.yy.zz.tt sizeof(MyClass1) Stack locale pp.qq.rr.ss // ip è un "interior pointer" pin_ptr<MyClass2 ^> pp = &hc; pin_ptr<MyClass2 ^> pp2 = pp; GC aggiorna i valori quando muove la memoria ll.mm.nn.oo // pp e pp2 sono "pinning pointers" Managed heap MyRefType %tr = *hc; // tr è un "tracking reference" memoria classica sempre ferma 30.00.00.00 size unknown pinned GC muove i blocchi di memoria Marshalling di stringhe Stringhe Ansi Managed Unmanaged Unmanaged Managed Marshal::StringToHGlobalAnsi Marshal::PtrToStringAnsi Stringhe Unicode Managed Unmanaged Unmanaged Managed PtrToStringChars Marshal::PtrToStringUni (#include <vcclr.h>) Stringhe COM (BSTR) Managed Unmanaged Unmanaged Managed Marshal::StringToBSTR Marshal::PtrToStringBSTR È tutto così semplice? ... quasi • Fin ad ora abbiamo visto che: – Creare immagini miste managed/unmanaged è semplice – Eseguire il marshalling dei parametri è semplice – Ci sono semplici strumenti per accedere alla memoria managed e unmanaged – L'interoperabilità è possibile in due modi: • P/Invoke esplicito (come in C#) • IJW (=It Just Works) eseguendo il marshalling dei parametri • E allora dov'è il problema? Mixed types are not supported public ref class RefClass { public: POINT pt; // unmanaged struct }; public class Native { public: System::String ^str; }; error C4368: cannot define 'pt' as a member of managed 'ManagedClass': mixed types are not supported error C3265: cannot declare a managed 'str' in an unmanaged 'Native' Tipi misti: tipi managed dentro tipi unmanaged • GCHandle • gcroot<> – necessita #include<vcclr.h> – non chiama automaticamente Dispose! • msclr::auto_gcroot<> – necessita #include <msclr\auto_gcroot.h> – chiama automaticamente la IDisposable::Dispose se esiste #include <vcclr.h> using namespace System; #include <msclr\auto_gcroot.h> using namespace msclr; using namespace System; public class Native1 { gcroot<String ^> str; }; public class Native2 { auto_gcroot<String ^> str; }; Tipi misti: tipi unmanaged dentro tipi managed • Brutta notizia: fin'ora nessun supporto ufficiale ma la soluzione è molto semplice .... • In una classe managed si può avere un puntatore unmanaged – ma è poi necessario gestire la sua distruzione (ciclo di vita) • Molto meglio scrivere una classe con template che gestisce il ciclo di vita del puntatore – Brandon Bray (uno degli ideatori della nuova sintassi) ne ha pubblicata una chiamata "Embedded" sul suo blog #include <windows.h> #include "Embedded.h" public ref class RefClass { Embedded<POINT> np; }; Cosa sono le calling convention? • Una sorta di contratto alla compilazione che prevede: – come passare gli argomenti delle funzioni ed il valore di ritorno – quali registri della CPU devono essere salvati • Le quattro convenzioni più usate oggi sono: ▪ __cdecl usato dalle librerie C e numerose API ▪ __stdcall conosciuta anche come "pascal", usata dalle Win32 API ▪ __fastcall usa i registri per passare gli argomenti ▪ __thiscall default per le chiamate a funzioni membro in C++ Cos'è il "double thunking"? • Quando si compila codice con metadati ogni funzione ha due entry-point: – uno con la calling-convention assegnata – uno con la calling-convention CLR • Quale viene usato? – se il codice è compilato con /clr, l'entry-point di base è un thunk alla chiamata CLR – se il codice è compilato senza /clr, l'entry point CLR è un thunk alla chiamata x86 • Come viene scelto l'entry-point da usarsi? – il compilatore è normalmente in grado di scegliere ma ... – non può scegliere se la chiamata è un puntatore a funzione – non può scegliere anche per le funzioni virtuali perché queste sono puntatori a funzioni Cos'è il "double thunking"? • Dove si presenta il problema? – Le funzioni virtuali compilate in IL avranno sempre un thunk da unmanaged a managed. Questo è inevitabile. – Se poi la chiamata viene fatta da codice managed, c'è un thunk supplementare: managed unmanaged managed Questo doppio passaggio si chiama "double thunking" • Esiste una soluzione? – La soluzione esiste solo se quella chiamata virtuale verrà solo chiamata dal mondo managed – In questo caso è sufficiente marcare la funzione con la convenzione __clrcall – forzando __clrcall si evita il double thunking virtual return-type __clrcall function-name(arguments); Un assembly, mixed language • Task complesso, nessun supporto di VS2005 • Più semplice se si disabilitano i precompiled headers in tutti i progetti VC++ (ma è comunque usarli) • La novità consiste nei .netmodule – Il .netmodule è identico ad un assembly ma senza metadati – per esempio non ha versione • Il netmodule viene ri-compilato al link time • Solo il linker di C++ ha questa capacità di ricompilazione a.cpp C++ Compiler a.obj EXE D:\>cl /c /clr a.cpp C++ Linker D:\>csc /t:module c.cs c.cs C# Compiler c.netmodule C++ Code C# Code Un assembly, mixed language • Esempio di una Winform C# che usa una business logic in C++/CLI EXE CppLogicClassLibrary CsFormClassLibrary CppStartWinform – Progetto 1: CppLogicClassLibrary • Per semplicità precompiled headers disabilitati • Si compila con VS.net Un assembly, mixed language • Progetto 2: CsFormClassLibrary – – – – Eliminato Program.cs, l'entry point sarà in C++/CLI Si referenzia CppLogicClassLibrary e si usano le classi Si compila in VS.NET solo per il controllo sintattico Necessario compilare a mano (ma si può lanciare make.bat come post-build action) Compilatore C# vogliamo un .netmodule dipendenza dal progetto C++/CLI csc /t:module /addmodule:..\CppLogicClassLibrary\debug\CppLogicClassLibrary.obj /resource:obj\Debug\CsFormClassLibrary.Form1.resources *.cs compilo tutti i sorgenti aggiungo le risorse (form) Un assembly, mixed language • Progetto 3: CppStartWinform – Progetto C++/CLI Winform a cui si toglie la form – Cambiare le opzioni da /clr:safe a /clr – Aggiungere nel Linker – Input – Additional il .netmodule di C# e l'obj di C++ – Aggiungere alla command line del linker l'opzione /LTCG – Funge solo da entry point per l'applicazione managed – Si può fare la build da VS.NET • Risultato: 1 Assembly EXE con dentro tre immagini miste native/managed – Ovviamente la dipendenza dal framework rimane Qual'è il futuro di ISO C++? • Ci sono problemi da risolvere per il cambio nell'evoluzione della crescita hardware – niente più grossi aumenti di velocità nelle CPU – aumento del numero di 'core' nelle CPU • L'accesso diretto alla memoria impedisce una gestione efficiente nel determinare i problemi di concorrenza • Work in progress su: – – – – – gestione automatica della concorrenza ("concurs") gestione asincrona ("Futures") type inference lambda functions Linq Domande?