PInvoke Interop Assistant: O εύκολος τρόπος για να μιλήσετε με unmanaged κώδικα


Στον κόσμο του .NET framework υπάρχουν πολλές φορές περιπτώσεις όπου είναι απαραίτητο ο managed κώδικας να μιλήσει με unamanaged κώδικα. Τυπικές τέτοιες περιπτώσεις είναι για παράδειγμα όταν υποχρεωνόμαστε να χρησιμοποιήσουμε legacy unmanaged κώδικα ή δεν καλυπτόμαστε από το .ΝΕΤ Framework API και πρέπει να χρησιμοποιήσουμε το COM ή το Win32 API. Όταν λοιπόν συμβαίνει κάτι τέτοιο, το CLR χρησιμοποιεί έναν μηχανισμό που ονομάζεται interop που αναλαμβάνει να γεφυρώσει τους δύο κόσμους κάνοντας αυτό που λέμε "marshaling" στα δεδομένα που ανταλλάσσονται. Η δουλειά αυτή είναι εύκολη όσο τα δεδομένα που περνούν πάνω από αυτή τη γέφυρα έχουν τη μορφή απλών structures. Οτιδήποτε άλλο, αρχίζει να δυσκολεύει το πράγμα με αποτέλεσμα να εμφανίζονται περίεργα exceptions και απρόβλεπτες συμπεριφορές.

Όσοι έχουν ασχοληθεί με το sport θα έχουν σίγουρα χρησιμοποιήσει το www.pinvoke.net, το wiki που τρέχει από την Red-Gate με σκοπό να υποστηρίξει το δωρεάν plug-in της. Εδώ και μερικές ημέρες υπάρχει το utility που ονομάζεται "PInvoke Interop Assistant" που έρχεται να βοηθήσει σε αυτόν τον τομέα. Το utility αποτελεί προϊόν συνεργασίας της ομάδας της VB.NET με αυτή του CLR και από μια πρώτη ματιά που του έριξα φαίνεται εξαιρετικό γι αυτή τη δουλειά!

Περισσότερα εδώ: http://blogs.msdn.com/vbteam/archive/2008/03/14/making-pinvoke-easy.aspx σε αυτό το link θα βρείτε και το σχετικό άρθρο στο MSDN που αναφέρει όλες τις φρικιαστικές λεπτομέρειες σχετικά με το marshaling.


Anonymous Types: Προσοχή στις διαφορές VB.NET – C#


Σε παλιότερο post μου, είχα αναφερθεί στα Anonymous Types, τα οποία μαζί με το Type Inference και τους Object Initializers αποτελούν στοιχεία απαραίτητα για το LINQ. Σε αντίθεση με το Type Inference (που είναι απλό ως concept και παρόμοιο στις δύο γλώσσες) και τους Object Initializers (κι αυτό παρόμοιο στις δύο γλώσσες) τα Anonymous Types κρύβουν μια σημαντική διαφορά (λέγε με breaking change) στην υλοποίησή τους! Πιστεύω ότι αυτή η διαφορά είναι απαραίτητο να τη γνωρίζει κανείς, είτε γράφει τακτικά και στις δύο γλώσσες, είτε πετύχει κάποιο code sample και πρέπει να το μετατρέψει από VB.NET σε C# ή το αντίστοφο.

Στη C#, όταν δύο anonymous types δημιουργούνται με properties που έχουν ίδιο όνομα, ίδια σειρά και ίδιο τύπο, τότε και τα anonymous types έχουν ίδιο τύπο. Αν αντίστοιχα objects έχουν properties με ίδιες τιμές, τότε τα δύο instances έχουν ίδιο τύπο και είναι equal. Δηλαδή στο

var w = new { FirstName = "Manos", LastName = "Kelaiditis" };
var x = new { FirstName = "Manos", LastName = "Kelaiditis" };
var y = new { FirstName = "Kelaiditis", LastName = "Manos" };
var z = new { LastName = "Kelaiditis" , FirstName = "Manos"};
Console.WriteLine(w.Equals(x) ? "Match" : "NoMatch");
Console.WriteLine(w.Equals(y) ? "Match" : "NoMatch");
Console.WriteLine(w.Equals(z) ? "Match" : "NoMatch");

μόνο το w είναι ίδιο με το x. Tο Equals στα anonymous types χρησιμοποιεί το hashcode του instance το οποίο υπολογίζεται προσθέτοντας το hashcode από κάθε member. Με λίγα λόγια, το hashcode εξαρτάται από τη δομή και τις τιμές. Τώρα το θέμα είναι το εξής: Σε συνέπεια του παραπάνω το hashcode χρησιμοποιείται σε πολλές περιπτώσεις, όπως όταν τα anonymous types μπαίνουν σε collections τύπου HashTable, όταν χρησιμοποιούμε grouping και filtering στα Linq queries αλλά ακόμα και στο data binding των collections. Ουσιαστικά τo hashcode παίζει το ρόλο κλειδιού. Αυτό σημαίνει ότι το hashcode δεν επιτρέπεται να αλλάξει ποτέ στη διάρκεια ζωής του object. Γι αυτό και οι σχεδιαστές της C# αποφάσισαν ότι τα Anonymous Types θα είναι Immutable. Όταν διαβάζετε Immutable θα σκεφτόσαστε "χαραγμένα σε πέτρα". Αυτό σημαίνει ότι αν τολμήσω να γράψω το παρακάτω:

z.LastName = "Georgiou";

θα πάρω το λάθος:

Error    1    Property or indexer ‘AnonymousType#1.LastName’ cannot be assigned to — it is read only   

καθώς τα instances από anonymous types είναι read only. Οι σχεδιαστές της C# θεώρησαν ότι τα "read-only" instances από anonymous types δεν φαίνεται να είναι ιδιαίτερο πρόβλημα αφού τα anonymous types έχουν περιορισμένη χρήση καθότι τυπικά δεν μπορούν να χρησιμοποιηθούν εκτός του context που έχουν δημιουργηθεί.

Τώρα, στη ομάδα της VB, είδαν ότι αν υιοθετούσαν το immutable χαρακτηριστικό για τα anonymous types θα ήταν (για λόγους συμβατότητας) μια απόφαση μονόδρομος που θα τους εμπόδιζε να είναι ευέλικτοι αφού αφενός λόγω late binding μπορεί να ανατραπεί το προηγούμενο και αφετέρου μελλοντικά θα προστεθούν χαρακτηριστικά όπως nominal anonymous types και dynamic interfaces.

Έτσι λοιπόν στη VB υπάρχει ο modifier "Key" για τα πεδία από τα anonymous types. Ο Key modifier κάνει το πεδίο read-only και καθοδηγεί τον compiler να κάνει override τις μεθόδους Equals και GetHashCode. Ας δούμε ένα παράδειγμα:

Dim x = New With {.FirstName = "Manos", .LastName = "Kelaiditis"}
Dim y = New With {.FirstName = "Manos", .LastName = "Kelaiditis"}
Dim z = New With {.LastName = "Manos", .FirstName = "Kelaiditis"}
Console.WriteLine(If(x.GetType() Is y.GetType(), "TypeMatch", "NoTypeMatch")) ' TypeMatch
Console.WriteLine(If(x.GetType() Is z.GetType(), "TypeMatch", "NoTypeMatch")) ' NoTypeMatch

Το πρώτο αποτέλεσμα δίνει TypeMatch ενώ το δεύτερο NoTypeMatch καθώς τα member του z είνα σε διαφορετική σειρά σε σχέση με το y. Μέχρι εδώ καλά. Ας δούμε το παρακάτω:

Dim x1 = New With {.FirstName = "Manos", .LastName = "Kelaiditis"}
Dim y1 = New With {.FirstName = "Manos", .LastName = "Kelaiditis"}
Console.WriteLine(If(x1.Equals(y1), "Match", "NoMatch")) ' NoMatch

Σε αντίθεση με αυτό που ισχύει στη C#, εδώ δεν παίρνουμε "Match" γιατί έχουμε mutable types αφού δεν έχουμε προσδιορίσει το Key. Απόδειξη είναι ότι μπορούμε άνετα να γράψουμε

x1.LastName = "Georgiou" 

Άρα, για να έχουμε αντιστοιχία με τη C# θα πρέπει να γράψουμε

Dim x2 = New With {Key .FirstName = "Manos", Key .LastName = "Kelaiditis"}
Dim y2 = New With {Key .FirstName = "Manos", Key .LastName = "Kelaiditis"}
Console.WriteLine(If(x2.GetType Is y2.GetType, "TypeMatch", "No TypeMatch"))
Console.WriteLine(If(x2.Equals(y2), "Match", "NoMatch"))

Βέβαια, anonymous types χρησιμοποιούμε και στα Linq queries. Τα παρακάτω είναι ισοδύναμα καθώς παράγονται immutable objects και στις δύο περιπτώσεις:

var query1 = from customer in customers
             select new { customer.FirstName, customer.LastName };
Dim query2 = From customer In customers _ 
             Select customer.FirstName, customer.LastName

Εκεί που υπάρχει διαφορά και χρειάζεται προσοχή είναι όταν χρησιμοποιούμε το New στη VB:

Dim query3 = From customer In customers _
             Select New With {.FirstName = customer.FirstName, _
                              .LastName = customer.LastName}

To παραπάνω query δεν είναι ισοδύναμο με το query2, παράγει mutable objects! Για να γράψουμε ισοδύναμο query με το query1 χρησιμοποιώντας το New, θα πρέπει να γράψουμε:

Dim query3 = From customer In customers _ 
             Select New With {Key .FirstName = customer.FirstName, _
 	                     Key .LastName = customer.LastName}

Τωρά, εκεί που ενδέχεται να κολλήσετε αν μεταφράζετε κώδικα από VB σε C# είναι αν σας τύχει το παρακάτω query:

Dim query = From prod In Products _
            Select New With {Key prod.Name, _
                             Key prod.CostPrice, _
                             prod.SalesPrice}

Παρατηρήστε ότι μόνο το Name και το CostPrice έχουν Key modifier ενώ το SalesPrice δεν έχει. Αυτό δεν μπορείτε να το περάσετε σε C# και θα πρέπει να καταφύγετε στη χρήση κανονικών κλάσεων. Βέβαια, το παραπάνω δίνει αρκετή ευελιξία καθώς για παράδειγμα το αποτέλεσμα του query μπορεί να γίνει data bound σε ένα grid και να επιτρέπονται οι αλλαγές στο πεδίο SalesPrice.


String.ToUpper και String.ToLower: Ξεχάστε τα για να κοιμάστε ήσυχοι


Λοιπόν αυτό είναι κάτι που δεν το είχα παρατηρήσει, μέχρι που πιάσαμε την κουβέντα περί FxCop και Globalization rules.

Αν χρησιμοποιήσετε το String.ToLower ή String.ToUpper και κάνετε ανάλυση του κώδικα μέσα από το Visual Studio θα χτυπήσει το CA1304 warning

Warning    9    CA1304 : Microsoft.Globalization : Because the behavior of ‘String.ToUpper()’ could vary based on the current user’s locale settings, replace this call in ‘Module1.Main()’ with a call to ‘String.ToUpper(CultureInfo)’. If the result of ‘String.ToUpper(CultureInfo)’ will be displayed to the user, specify ‘CultureInfo.CurrentCulture’ as the ‘CultureInfo’ parameter. Otherwise, if the result will be stored and accessed by software, such as when it is persisted to disk or to a database, specify ‘CultureInfo.InvariantCulture’.    C:\Users\Manos\AppData\Local\Temporary Projects\ConsoleApplication1\Module1.vb    21    ConsoleApplication1

Αυτό είναι ένα unicode πρόβλημα που οφείλεται στο ότι σε πολλά cultures η μετατροπή από Lower σε Upper και ξανά σε Lower δεν είναι κυκλική! Δηλαδή αν ξεκινήσεις από ένα string πεζών, το μετατρέψεις σε κεφαλαία και ξανά σε πεζά, το πρώτο με το τρίτο string δεν είναι πάντοτε ίδια. Στα Ελληνικά υπάρχει πρόβλημα με το τελικό Σ και τα τονούμενα πεζά με διαλυτικά. Ας πούμε, το

"ΐΰ" = "ΐΰ".ToUpper.ToLower

δίνει False!

Σε άλλες γλώσσες που χρησιμοποιούν latin characters, το πρόβλημα είναι πιο έντονο (πχ βλ. εδώ σχετικά με το Τούρκικο locale).

Το πρόβλημα είναι ότι υπάρχει περίπτωση, κώδικας που παίρνει security αποφάσεις βάσει μετατροπών .ToLower και .ToUpper να μην συμπεριφερθεί σωστά. Γι αυτό καλό είναι

  • Να χρησιμοποιηθεί η String.ToUpper(CultureInfo) και String.ToLower(CultureInfo)
  • Να χρησιμοποιηθεί η String.Equals η οποία διαθέτει overloaded κλήση που καθορίζει το culture sensitivity
  • Να χρησιμοποιηθεί η String.Compare με το IgnoreCase option

Περισσότερα για το θέμα αυτό:

http://msdn2.microsoft.com/en-us/library/bb386042.aspx

http://blogs.msdn.com/michkap/archive/2005/04/04/405174.aspx

http://msdn2.microsoft.com/en-us/library/5bz7d2f8(VS.71).aspx

Το πρόβλημα αυτό ανάγεται σε επίπεδο .NET Framework, δηλαδή είναι ανεξάρτητο γλώσσας, οπότε παίξτε by-the-book για να έχετε το κεφάλι σας ήσυχο…


Late Binding: Revisited (και γιατί πρέπει να μας νοιάζει)


Σε συνέχεια του post περί late binding, ας δούμε πως μπορούμε να πετύχουμε σωστό late binding, χωρίς να καταφύγουμε στο quick-and-dirty Option Strict Off.

H μαγική λέξη είναι "Reflection". Τι είναι το Reflection μέσα σε 10»: Είναι ένας μηχανισμός που επιτρέπει την αναζήτηση πληροφοριών σχετικές με μια κλάση κατά το runtime. Δηλαδή, μπορούμε κατά το runtime να βρούμε τι members έχει μια κλάση, τι παραμέτρους δέχονται και τι τιμές επιστρέφουν. Αυτό είναι εξαιρετικά χρήσιμο! Είναι ο μηχανισμός πάνω στον οποίο βασίζεται τo Intellisence και τώρα θα τον χρησιμοποιήσουμε για late binding.

Ας ορίσουμε μια κλάση:

Class TestClass
    Public Function DoSomething(ByVal value As Integer) As Integer
        Return value * 2
    End Function
End Class

Και τώρα ας δούμε πώς θα τη χρησιμοποιήσουμε:

        Dim objTest As New TestClass
        Dim myType As Type
        Dim param(0) As Object
        Dim rslt As Integer

        Dim parameter As String = 5
        Dim functionName As String = "DoSomething"

        param(0) = CType(parameter, Integer)                myType = objTest.GetType 

        rslt = CType(myType.InvokeMember(functionName, _
                     Reflection.BindingFlags.Default Or _
                     Reflection.BindingFlags.InvokeMethod, _
                     Nothing, objTest, param), Integer)

        Console.WriteLine(rslt)

 

Στην πρώτη ομάδα Dim ορίζουμε το object που θα χρησιμοποιήσουμε, και το array από παραμέτρους που θα περάσουμε ως όρισμα στη μέθοδο που θα καλέσουμε. Στη δεύτερη ομάδα Dim ορίζουμε το όνομα της μεθόδου και τις τιμές των παραμέτρων. Κατόπιν με την GetType διαβάζουμε πληροφορίες της κλάσης που ορίσαμε από το manifest. Τι είναι το manifest σε 20»: Πρόκειται για ένα σύνολο από πληροφορίες που εμπεριέχονται σε κάθε assembly (assembly ονομάζεται στο .NET το παραγόμενο αποτέλεσμα του compilation – το dll ή το exe) που αφορά στην περιγραφή του ίδιου του assembly, πχ όνομα του assembly, έκδοση, culture και – αυτό που μας ενδιαφέρει -type reference πληροφορίες).

Οπότε ερχόμαστε και στο ζουμί:

Πάνω στο myType μπορούμε να καλέσουμε την InvokeMember η οποία (όπως και λέει το όνομά της) θα κάνει την κλήση βάσει των ορισμάτων: Το πρώτο είναι το όνομα της μεθόδου. Το δεύτερο, τα BindingFlags, καθοδηγούν το CLR στο πώς θα κάνει το binding. To τρίτο ορίζει το binder. Με το Nothing αφήνουμε το default binder (με custom binders έχουμε επιπρόσθετο έλεγχο κατά την επιλογή της μεθόδου κατά το Invoke). Με την τέταρτη παράμετρο ορίζουμε το πραγματικό object που θα χρησιμοποιηθεί και με την τελευταία περνάμε τις παραμέτρους της μεθόδου. Γενικά, ο κώδικας παραπάνω είναι απλουστευμένος με την έννοια ότι ξέρουμε ήδη ότι η μέθοδος μας επιστρέφει έναν ακέραιο. Στην πραγματικότητα, θα μπορούσαμε να πάρουμε κι αυτή τη πληροφορία μέσω Reflection.

Φυσικά, όλα τα παραπάνω παίζουν και στη C# δηλαδή η διαφορά που έχει η VB με τη C# στο late binding είναι ο πρώτος τρόπος που είδαμε στο προηγούμενο post. Αυτός ο τρόπος είναι by file late binding καθώς καθορίζεται με το Option Strict setting που είναι per file. Από την άλλη μεριά έχει το πλεονέκτημα ότι καθιστά την VB.NET ένα ιδανικό εργαλείο για εφαρμογές office automation και γενικότερα COM interop καθώς απλουστεύει κατά πολύ το implementation (αρκεί να ξέρει κανείς τι κάνει, αυτό που λέμε Static Typing Where Possible, Dynamic Typing When Needed)

To late binding παρότι ως θέμα είναι παλιό, αποτελεί ένα βασικό μηχανισμό ώστε η VB και η C# να αποκτήσουν dynamic χαρακτηριστικά και γι αυτό τώρα αποκτά νέα δυναμική (pun intended). Στην τωρινή μορφή με χρήση Reflection δεν είναι και τόσο εύκολη διαδικασία και γι αυτό ήδη το team της C# σκέφτεται το πως θα ενσωματώσει το late binding στη γλώσσα. Όπως και να έχει, ένα πράγμα είναι σίγουρο: οι εξελίξεις στις γλώσσες του .NET Framework θα είναι συνεχείς και πολύ ενδιαφέρουσες. Αν μπορούσαμε να στρβλώσουμε και τον χρόνο για να βγάλω 30 ώρες στο 24ωρο, θα ήταν ακόμα καλύτερα.


"Red" bits & "Green" bits… Όλα τα bits δεν είναι ίδια!


Ο τίτλος δεν έχει σχέση με τον Ολυμπιακό και τον Παναθηναϊκό. Το κόκκινο και το πράσινο χρησιμοποιούνται για να χαρακτηρίσουν τον ρόλο που παίζει κάθε assembly στο οικοδόμημα του .NET Framework.

Κάνοντας μια μικρή αναδρομή, ξεκινήσαμε από το .ΝΕΤ Framework 1.0, μετά ήρθε το .ΝΕΤ Framework 1.1 και κατόπιν το .ΝΕΤ Framework 2.0. Το 1.1 αντικατέστησε το 1.0 ενώ το 2.0 μπορεί να τρέχει παράλληλα με το 1.1. Μετέπειτα, με τα Windows Vista, ήρθε το 3.0 που ουσιαστικά είναι επιπρόσθετα assemblies τα οποία συμπληρώνουν αυτά του 2.0, καθώς τo 3.0 χρειάζεται το 2.0 και δεν είναι "αυτόνομο". Σε λίγο (ή σε πολύ… ποιός να ξέρει…), όταν το Visual Studio "Orcas" βγει σε παραγωγή, θα έρθει το .ΝΕΤ Framework 3.5 το οποίο όμως θα κάθεται πάνω στο 3.0.

Και εδώ είναι που προκύπτει ένα πρόβλημα… Καθώς το 3.5 θα βασίζεται πάνω στο 3.0 είναι αναγκαίο να υπάρχει ο μέγιστος βαθμός συμβατότητας ώστε οι εφαρμογές που φτιάχτηκαν πάνω στο 3.0 να μην αντιμετωπίσουν προβλήματα όταν τρέξουν στο 3.5. Αυτό είναι πολύ καλό γιατί θα γλυτώσει πολύ κόσμο και πολλές εταιρείες από διάφορα προβλήματα support. Όμως συνεπάγεται και κάποιες δυσάρεστες συνέπειες. Δεν μπορούμε να περιμένουμε Service Packs μέχρι να κυκλοφορήσει το "Orcas" καθώς η ομάδα του "Orcas" δουλεύει πάνω σε συγκεκριμένη έκδοση .ΝΕΤ Framework, την 3.0 των Windows Vista.

Η ομάδα του Soma Somasegar έχει καθορίσει ένα μοντέλο που το έχει ονομάσει "Red and Green". Σύμφωνα με αυτό, ότι assembly έχει γίνει ήδη release (WPF, WCF, WF και .NET Framework 2.0) χαρακτηρίζεται ως "Red" ενώ όλες οι νέες προσθήκες από assemblies ως "Green". Στόχος τους είναι να πειράζουν όσο το δυνατόν λιγότερο τα Red assemblies και τα κύρια νέα χαρακτηριστικά να εισαχθούν ως "Green" assemblies ώστε να εξασφαλισθεί η συμβατότητα των γραμμένων εφαρμογών. Ο χρόνος θα δείξει αν θα καταφέρουν να το πετύχουν. Εσείς στο μεταξύ, μπορείτε πλέον να αντιληφθείτε πότε και αν θα διορθωθεί εκείνο το bug που υποβάλατε στο http://support.microsoft.com/ και σας έχει βγάλει την πίστη…

Για περισσότερα περί του "Red and Green", ρίξτε μια ματιά σε αυτό εδώ: http://blogs.msdn.com/somasegar/archive/2006/05/18/601354.aspx