none
Structures génériques pour méthodes natives / Windows API RRS feed

  • Question

  • Bonjour, je développe un utilitaire en C# qui fait appel à des méthodes natives de l'API Windows (Ex: NetUserGetInfo/NetUserSetInfo/NetUserModalsGet/NetUserModalsSet).

    En gros, on passe à ces fonctions un "level" et un pointeur sur une structure qui correspond au level et qui sera rempli par la fonction.

    Ex: http://msdn.microsoft.com/en-us/library/windows/desktop/aa370654(v=vs.85).aspx

    Il s'avère qu'il y'a parfois une 30aine de level et donc de structures qui peuvent être appelées.

    (Ex: de USER_INFO_0 à USER_INFO_1053: http://msdn.microsoft.com/en-us/library/windows/desktop/aa370961(v=vs.85).aspx)

    J'aimerai faire une sorte d'interface ou de wrapper à ces fonctions pour avoir des méthodes génériques. Prenons un exemple plus parlant:

    Au lieu de faire 30 surcharges par méthode

    NativeNetUserGetInfo(string servername, string username, uint level, out USER_INFO_1 ui1);

    NativeNetUserGetInfo(string servername, string username, uint level, out USER_INFO_2 ui2);

    NativeNetUserGetInfo(string servername, string username, uint level, out USER_INFO_3 ui3);

    ...

    J'aimerai pouvoir faire un structure générique qui puisse être utilisée et appelée

    comme suit:

    NativeNetUserGetInfo(string servername, string username, uint level, out USER_INFO ui);

    ui pouvant être USER_INFO_1/USER_INFO_2/USER_INFO_3.

    Des idées de comment faire ? Je suis preneur si vous avez des tutoriels ou de la doc qui puisse m'expliquer ;) Ou du code c'est encore mieux! :)

    lundi 4 juin 2012 15:33

Réponses

  • Compte tenu des principes de la POO, on ne devrait même pas avoir besoin de "caster". A l'exécution le type réel de l'objet est bien USER_INFO1 et je pense donc possible que cela marche (même si comme je disais je ne me souviens pas avoir testé ou même vu ce cas de figure) si le marshalling se base bien sur le type réel de la classe.

    Au pire les surcharges me paraissent la méthode la plus élégante. Après tout on a vraiment une même fonction que l'on peut appeler avec une foultitude de types différents. Donc dans ce cas, exprimer explicitement les différentes signatures possibles me semble relativement logique.

    Sinon on doit pouvoir utiliser un pointeur mais on va alors contre la nature fortement typé du langage.

    Et donc par je pense que #1 exprime mieux la réalité que #2 qui ne fait que minimise le nombre de lignes au détriment d'exprimer ce que l'on a réellement...


    Please always mark whatever response solved your issue so that the thread is properly marked as "Answered".

    lundi 4 juin 2012 17:24
    Modérateur
  • Pas de souci, pour moi répondre a toujours été est une bonne façon d'apprendre ;-)

    J'avais d'ailleurs déjà fait un petit essai pour voir, que j'ai un peu complété. Cela devrait permettre d'ajouter assez facilement la prise en charge d'une autre structure. Notamment un dictionnaire me permet de récupérer le "level" qui correspond au type de structure et la fonction PtrToArray<T> doit pouvoir être réutilisée dans les différents appels. On devait donc au final pouvoir avoir une fonction par appel d'API indépendamment des variations au niveau des structures. Une petite amélioration pourrait être de garder le principe d'une classe vide parent ne serait ce que pour pouvoir mettre une contrainte sur le type générique (qui indquerait que T doit nécessairement hériter de cette classe ce qui éviterait à l'utilisateur de mettre n'importe quoi comme type en paramètre). Cela donne donc :

            static class NetApi
            {
                [DllImport("netapi32", EntryPoint = "NetUserEnum")]
                private static extern int NetUserEnum(string servername, UInt32 level, UInt32 filter, out IntPtr bufptr, UInt32 prefmaxlen, ref UInt32 entriesread, ref UInt32 totalentries, IntPtr resume_handle);
                [DllImport("netapi32")]
                private static extern int NetApiBufferFree(IntPtr bufptr);
                private static Dictionary<Type, uint> levels = new Dictionary<Type, uint> { { typeof(_USER_INFO_0), 0 } };
                private static void PtrToArray<T>(IntPtr p,T[] array)
                {
                    var current = p;
                    for (int i = 0; i < array.Length; i++)
                    {
                        array[i] = (T)Marshal.PtrToStructure(current, typeof(T));
                        current += Marshal.SizeOf(typeof(T));
                    }
                }
                public static IEnumerable<T> UserEnum<T>(string serverName)
                {
                    //string serverName = null;
                    //uint level = 0; //Just get the account names
                    uint filter = 0;//all acc. names
                    uint prefmaxlen = 65535;
                    IntPtr bufPtr;// = Marshal.AllocCoTaskMem((int)prefmaxlen);
                    uint entriesread = 0;
                    uint totalentries = 0;
                    IntPtr resume = IntPtr.Zero;
                    int errorstatus = NetUserEnum(serverName,levels[typeof(T)], filter, out bufPtr, prefmaxlen, ref entriesread, ref totalentries, resume);
                    T[] ui = new T[entriesread];
                    PtrToArray<T>(bufPtr, ui);
                    Debug.Assert(NetApiBufferFree(bufPtr) == 0);
                    return ui;
                }
                [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
                public struct _USER_INFO_0
                {
                    public string usri0_name;
                }
            }
    
            static void Main(string[] args)
            {
                foreach (var u in NetApi.UserEnum<NetApi._USER_INFO_0>(null))
                {
                    Console.WriteLine("["+u.usri0_name+"]");
                }
                Console.ReadLine();
            }
    Une petite boulette dans le code. Je pense qu'il est préférable de récupérer le résultat de APiFree et de le tester ensuite (sino n en release, le Debug.Assert ne sera pa compilé et le buffer pas libéré).

    Please always mark whatever response solved your issue so that the thread is properly marked as "Answered".


    mercredi 6 juin 2012 10:01
    Modérateur
  • Au final j'ai une version qui fonctionne.

    Si quelqu'un voit des erreurs ou des améliorations à apporter je suis preneur ;)

            public static void UserAdd<T>(string ServerName, ref T UserInfo)
            {
                uint NetStatusApi;
                uint ParmErr;
                IntPtr UserInfoPtr;

                UserInfoPtr = Marshal.AllocHGlobal(Marshal.SizeOf(UserInfo));

                try
                {
                    Marshal.StructureToPtr(UserInfo, UserInfoPtr, false);
                    NetStatusApi = NetUserAdd(ServerName, UserInfoLevels[typeof(T)], UserInfoPtr, out ParmErr);
                    if (NetStatusApi != NERR_Success)
                    {
                        throw new Win32Exception(Marshal.GetLastWin32Error());
                    }
                }
                finally
                {
                    Marshal.FreeHGlobal(UserInfoPtr);
                }
            }




    jeudi 7 juin 2012 22:23
  • Au temps pour moi la doc http://msdn.microsoft.com/fr-fr/library/system.runtime.interopservices.dllimportattribute.aspx indique effectivement que DllImport ne prends pas en charge les types génériques. Je pense que le mieux est d'en rester là pour l'instant. Je posterai un lien ici si je fais un test et si je trouve qq chose... DllImport se base peut-être seulement sur la signature de la fonction ce qui exclut les types génériques et l'héritage...


    Please always mark whatever response solved your issue so that the thread is properly marked as "Answered".


    vendredi 8 juin 2012 12:40
    Modérateur
  • PtrToStructure copie les données d'un emplacement non managé vers une structure ou une classe managée. On peut donc utiliser la valeur de retour comme bon non semble en code managée, puis libérer la mémoire non managée, les données ayant été copiées. Add ne joue aucun role dans ce mécanisme, cela fonctionne juste comme sur n'importe quel aure objet managé. C'est PtrToStructure qui fait la transition.

    Ma compréhension est que la valeur -1 est reconnue spécifiquement et ne pas va allouer la totalité de la mémoire correspondante mais autant que nécessaire pour stocker la totalité des données (à voir x utilisateurs x y octets en moyenne, à priori ce ne va pas chercher bien loin même pour beaucoup d'utilisateurs) ce qui semblait être la stratégie d'origine.

    Si effectivement on passe de -1 à 65535 on peut effectivement faire un do {} while (status==MORE_DATA) pour lire les données tant qu'il y a des encore des données...


    Please always mark whatever response solved your issue so that the thread is properly marked as "Answered".

    • Marqué comme réponse SylvainnnD lundi 11 juin 2012 11:31
    lundi 11 juin 2012 08:37
    Modérateur

Toutes les réponses

  • Bonjour,

    Peut-être en utilisant des classes qui hériterait toutes d'une même classe de base USER_INFO (même vide ?) au lieu d'utiliser des structures comme actuellement (mais je ne me souviens pas avoir essayé ou avoir vu un essai analogue, je ne sais pas à 100% si le marshalling se base sur le type réel de la variable ou se base sur le type présent dans la déclaration)...

    Accessoirement ce ne sont pas des infos que l'on pourrait récupérer avec AD ? http://msdn.microsoft.com/en-us/library/wwzcae1f(v=vs.80)


    Please always mark whatever response solved your issue so that the thread is properly marked as "Answered".




    lundi 4 juin 2012 16:42
    Modérateur
  • Déjà merci pour ta réponse,

    On peut le faire de différente manière (avec WMI ou directoryservices) mais j'ai absolument besoin d'utiliser l'api Windows native avec du marshalling & compagnie ;-) 

    Comme je débute en C# donc il me manque certaines connaissances.

    Admettons que je crée une classe de base USER_INFO vide, je fais bien hériter mes classes avec du public class USER_INFO1: USER_INFO puis je caste un objet USER_INFO1 en USER_INFO quand je passe l'objet à la méthode native?

    Pour le marshalling, je commence à m'en sortir pour les structures et les types complexes, mais j'ai pas encore trop zyeuter pour les classes ! (je crois que j'ai jamais vu).

    Sinon admettons que je ne puisse pas (ARGHHHHH), quelle serait la méthode la moins porc pour faire ce que je veux? Faire des surcharges de méthodes ? Faire une interface avec des if / else ? D'autres idées ? :-)


    lundi 4 juin 2012 16:59
  • Compte tenu des principes de la POO, on ne devrait même pas avoir besoin de "caster". A l'exécution le type réel de l'objet est bien USER_INFO1 et je pense donc possible que cela marche (même si comme je disais je ne me souviens pas avoir testé ou même vu ce cas de figure) si le marshalling se base bien sur le type réel de la classe.

    Au pire les surcharges me paraissent la méthode la plus élégante. Après tout on a vraiment une même fonction que l'on peut appeler avec une foultitude de types différents. Donc dans ce cas, exprimer explicitement les différentes signatures possibles me semble relativement logique.

    Sinon on doit pouvoir utiliser un pointeur mais on va alors contre la nature fortement typé du langage.

    Et donc par je pense que #1 exprime mieux la réalité que #2 qui ne fait que minimise le nombre de lignes au détriment d'exprimer ce que l'on a réellement...


    Please always mark whatever response solved your issue so that the thread is properly marked as "Answered".

    lundi 4 juin 2012 17:24
    Modérateur
  • Merci Patrice, je vais regarder pour les classes,

    Sinon je ferai de la surcharge! :)

    PS: Je laisse la question ouverte au cas où quelqu'un d'autre aurait une idée ou saurait pour le marshalling!


    lundi 4 juin 2012 17:33
  • J'ai de nouvelles questions dans la prolongation des autres :-)
    On verra si j'ouvre un nouveau post ou pas :-)

    Pour faire simple, les fonctions de l'API windows que j'utilise
    peuvent renvoyer des structures différentes.

    Par exemple NetUserEnum peut pointer/retourner des USER_INFO_0, USER_INFO_1...USER_INFO_24
    en fonction du "level".

    J'aimerai faire une fonction générique sans surcharge de méthode qui renvoie une liste
    de ces objets une fois parcourus / énumérés.

    Ma question est... pour ne pas faire de surcharge de méthode dans ce cas là, quelle est
    la meilleure méthode à utiliser? Existe-t-il la possibilité de retourner une liste de type
    générique qui contient mes structures qui peuvent être différentes en fonction de ce fameux level?
    Est ce que ArrayList est une bonne option? Est ce qu'il existe d'autre type que ArrayList qui peuvent être sexy?

    List NetUserEnum(...)?
    ArrayList NetUserEnum(...)?
    mardi 5 juin 2012 08:46
  • Ou alors cf http://www.dotnet247.com/247reference/msgs/38/193352.aspx (le point clé étant l'utilisation de Marshal.PtrToStructure) pour utiliser des pointeurs ce qui au final sera peut-être tout de même globalement plus simple.

    De toute façon, c'est masqué j'imagine par une API de plus haut niveau exposée à l'appli...

    Pour le cas particulier de la struture, on pourrait peut-être exposé un IEnumerable pour faire le moins de supposition possible sur la structure réelle...


    Please always mark whatever response solved your issue so that the thread is properly marked as "Answered".


    mardi 5 juin 2012 10:03
    Modérateur
  • // NetUserEnum [DllImport("netapi32.dll", EntryPoint = "NetUserEnum", CharSet = CharSet.Unicode, SetLastError = true)] public static extern uint NetUserEnum( [In] [MarshalAsAttribute(UnmanagedType.LPWStr)] string servername, [In] uint level, [In] uint filter, [Out] out IntPtr bufptr, [In] uint prefmaxlen, [Out] out uint entriesread, [Out] out uint totalentries, [In, Out] ref uint resume_handle ); [...] public static void TestNetUserEnum(string servername, uint level=1, uint filter=0) { uint status; IntPtr bufptr; IntPtr iter; uint prefmaxlen = uint.MaxValue; uint entriesread; uint totalread; uint resume_handle = 0; USER_INFO_3 ui; uint i; do { status = NetUserEnum(servername, level, filter, out bufptr, prefmaxlen, out entriesread, out totalread, ref resume_handle); if ((status == NERR_Success) || (status == ERROR_MORE_DATA)) { for (i = 0, iter = bufptr; (i < entriesread); i++) { if (iter != IntPtr.Zero) { ui = (USER_INFO_3)Marshal.PtrToStructure(iter, typeof(USER_INFO_3)); Console.WriteLine("========="); Console.WriteLine("ui.usri3_name: {0}", ui.usri3_name); Console.WriteLine("ui.usri3_password: {0}", ui.usri3_password); Console.WriteLine("ui.usri3_password_age: {0}", ui.usri3_password_age); Console.WriteLine("ui.usri3_home_dir: {0}", ui.usri3_home_dir); Console.WriteLine("ui.usri3_priv: {0}", ui.usri3_priv); Console.WriteLine("ui.usri3_comment: {0}", ui.usri3_comment); Console.WriteLine("ui.usri3_flags: {0}", ui.usri3_flags); Console.WriteLine("ui.usri3_script_path: {0}", ui.usri3_script_path);

    iter = (IntPtr)((int)iter + Marshal.SizeOf(typeof(USER_INFO_3))); } } } // FREE if (bufptr != IntPtr.Zero) { NativeMethods.NetApiBufferFree(bufptr); } } while (status == ERROR_MORE_DATA);

    Voilà mon code actuellement, pour l'instant j'affiche juste une structure USER_INFO3

    En gros pour avoir quelque chose de relativement générique je pense qu'il faut:

    - passer le level de la structure à ma fonction Test (comme actuellement),

    - ajouter un paramètre qui est une référence à une liste générique qui puisse contenir n'importe quel type de structure ou à un objet IEnumerable.

    Maintenant je dois dire que je sais pas trop faire ça vu que je débute.

    Pour une liste générique ça serait ref list<T> users? ou c'est obligatoirement ref list<USER_INFO3> users? (dans ce cas ça m'arrange pas).

    Pour le type IEnumerable, j'aime bien l'idée, mais quel type d'objet est IEnumerable? ArrayList avec ref ArrayList users? y'en a d'autres?

    Voilà sinon avant d'ajouter les structures à ces listes (avant iter), faut-il les copier, ou il est possible de juster faire un append sur le pointeur?

    PS: je pose les questions pour essayer d'avoir le truc le mieux possible et parce que tu es super sympa de me répondre Patrice! :)

    mercredi 6 juin 2012 08:20
  • Pas de souci, pour moi répondre a toujours été est une bonne façon d'apprendre ;-)

    J'avais d'ailleurs déjà fait un petit essai pour voir, que j'ai un peu complété. Cela devrait permettre d'ajouter assez facilement la prise en charge d'une autre structure. Notamment un dictionnaire me permet de récupérer le "level" qui correspond au type de structure et la fonction PtrToArray<T> doit pouvoir être réutilisée dans les différents appels. On devait donc au final pouvoir avoir une fonction par appel d'API indépendamment des variations au niveau des structures. Une petite amélioration pourrait être de garder le principe d'une classe vide parent ne serait ce que pour pouvoir mettre une contrainte sur le type générique (qui indquerait que T doit nécessairement hériter de cette classe ce qui éviterait à l'utilisateur de mettre n'importe quoi comme type en paramètre). Cela donne donc :

            static class NetApi
            {
                [DllImport("netapi32", EntryPoint = "NetUserEnum")]
                private static extern int NetUserEnum(string servername, UInt32 level, UInt32 filter, out IntPtr bufptr, UInt32 prefmaxlen, ref UInt32 entriesread, ref UInt32 totalentries, IntPtr resume_handle);
                [DllImport("netapi32")]
                private static extern int NetApiBufferFree(IntPtr bufptr);
                private static Dictionary<Type, uint> levels = new Dictionary<Type, uint> { { typeof(_USER_INFO_0), 0 } };
                private static void PtrToArray<T>(IntPtr p,T[] array)
                {
                    var current = p;
                    for (int i = 0; i < array.Length; i++)
                    {
                        array[i] = (T)Marshal.PtrToStructure(current, typeof(T));
                        current += Marshal.SizeOf(typeof(T));
                    }
                }
                public static IEnumerable<T> UserEnum<T>(string serverName)
                {
                    //string serverName = null;
                    //uint level = 0; //Just get the account names
                    uint filter = 0;//all acc. names
                    uint prefmaxlen = 65535;
                    IntPtr bufPtr;// = Marshal.AllocCoTaskMem((int)prefmaxlen);
                    uint entriesread = 0;
                    uint totalentries = 0;
                    IntPtr resume = IntPtr.Zero;
                    int errorstatus = NetUserEnum(serverName,levels[typeof(T)], filter, out bufPtr, prefmaxlen, ref entriesread, ref totalentries, resume);
                    T[] ui = new T[entriesread];
                    PtrToArray<T>(bufPtr, ui);
                    Debug.Assert(NetApiBufferFree(bufPtr) == 0);
                    return ui;
                }
                [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
                public struct _USER_INFO_0
                {
                    public string usri0_name;
                }
            }
    
            static void Main(string[] args)
            {
                foreach (var u in NetApi.UserEnum<NetApi._USER_INFO_0>(null))
                {
                    Console.WriteLine("["+u.usri0_name+"]");
                }
                Console.ReadLine();
            }
    Une petite boulette dans le code. Je pense qu'il est préférable de récupérer le résultat de APiFree et de le tester ensuite (sino n en release, le Debug.Assert ne sera pa compilé et le buffer pas libéré).

    Please always mark whatever response solved your issue so that the thread is properly marked as "Answered".


    mercredi 6 juin 2012 10:01
    Modérateur
  • Wouah le code est super beau, en plus ça devient IEnumerable donc je crois qu'on peut utiliser LINQ dessus!

    Bon après je dois dire que je comprends pas tout, ça fait que 2 semaines que je me suis mis à c# :-)

    UserEnum<T>(string serverName)
    private static void PtrToArray<T>(IntPtr p,T[] array)
    PtrToArray<T>(bufPtr, ui);

    Je connais pas ces notations (nom de méthode<T>), je vais regarder un peu de doc, ça fera pas de mal! :-)



    mercredi 6 juin 2012 12:03
  • Bon au final c'est pas très dur à comprendre :-p ('fin je crois, c'est juste la notation pour spécifier le type générique qu'on utilise).

    Donc le code que tu as pondu est super, je pense faire quelques petites modifications mais je sais pas si c'est possible:

    En gros, UserEnum retourne bien un status de NetUserEnum() et le IEnumerable est passé en paramètre de la fonction (mais je sais pas trop comment faire?)

    ça serait par exemple "ref IEnumerable<T> UserInfo" en paramètre?

    mercredi 6 juin 2012 12:21
  • Oui c'est cela. La notation <T> permet d'écrire du code sans connaitre le type précis dont il s'agit et qui sera précisé quand on mettra en oeuvre effectivement un appel à ce code, ce qui permet donc d'écrire du code générique qui pourra s'appliquer ensuite à des types divers et variés.

    Par contre je ne suis pas sûr de bien comprendre la modif que tu veux faire. On peut passer effectivement un paramètre de ce style mais je ne suis pas sûr de comprendre à quelle fonction tu veux le passer. En dehors de ton contexte :

      static void WriteEnum<T>(IEnumerable<T> list)
            {
                int i = 0;
                foreach (var o in list)
                {
                    Console.WriteLine(i+":"+o);
                    i++;
                }
            }
    
            static void Main(string[] args)
            {
                string[] a = { "A", "B" };
                int[] b = { 1, 2 };
                List<Char> c = new List<Char>(){ 'X', 'Y', 'Z' };
                WriteEnum(a);
                WriteEnum(b);
                WriteEnum(c);
                Console.ReadLine();
            }

    Donc la fonction WriteEnum accepte une énumération de valeurs génériques qui vont être affichées sur la console, et le code va s'appliquer concrètement aussi bien à un tableau de chaîne, d'entier ou à une liste de caractères ou tout autre énumération de valeurs, etc...

    Dans les appels à WriteEnum on n'a pas besoin de préciser le type car le compilateur a assez d'information pour le déduire tout seul (on passe un IEnumerable<T> donc T est un string dans le premier cas etc...).


    Please always mark whatever response solved your issue so that the thread is properly marked as "Answered".



    mercredi 6 juin 2012 13:16
    Modérateur
  • Merci pour ton exemple, c'est clair et j'espère comprendre! :)

    En fait j'aimerai que UserEnum me renvoie le NT_STATUS_API du vrai NetUserEnum pour avoir le code de retour de la fonction native.

    (Je sais je suis chiant :-) ). Si je veux faire ça, la seule possibilité que je vois c'est au lieu de retourner le IEnumerable<T>, de le passer en paramètre à la fonction UserEnum. Par contre j'ai peur de mal prototyper:

    Moi je ferais comme ça:

    public static uint UserEnum<T>(string serverName, out IEnumerable<T> UserInfo)

    Et dans le code de la méthode je ferai comme ça et:

    T[] UserInfo = new T[entriesread]; 
    PtrToArray<T>(bufPtr, UserInfo);
    
    return status;

    Puis j'appelerai la méthode comme suit:

    IEnumerable<_USER_INFO_0> UserInfo;

    ...

    status = UserEnum<_USER_INFO_0>(serverName, out UserInfo)



    mercredi 6 juin 2012 13:34
  • Vite fait cela me parait ok. Il y a un problème quand tu essayes ? UserEnum(serverName,out UserInfo) devrait être suffisant (comme je disais plus haut le compilateur peut se baser sur la déclaration de UserInfo pour savoir quel est le type T que l'on utilise dans ce contexte)...

    Sur le plan du principe, générer une exception me parait mieux. L'idée est que l'on peut ignorer une valeur de retour donc il faut tester explictement la valeur de retour pour voir quelle est la situation. Par contre si on génère une exception, on est obligé de faire qq chose si on veut la traiter ou l'ignorer (cf Win32Exception).


    Please always mark whatever response solved your issue so that the thread is properly marked as "Answered".

    mercredi 6 juin 2012 16:02
    Modérateur
  • Au final, je vais faire comme tu me dis avec les exceptions.

    Sinon j'essaye de faire la fonction NetUserAdd, mais j'ai un problème :-)

    public static void UserAdd<T>(string ServerName, ref FIXME<T> UserInfo)
            {
                uint Status;
                uint ParmErr = 0;
                IntPtr UserInfoPtr = IntPtr.Zero;
    
                Marshal.StructureToPtr(UserInfo, UserInfoPtr, false);
                Status = NetUserAdd(ServerName, UserInfoLevels[typeof(T)], UserInfoPtr, out ParmErr);
            }

    Tout bêtement, pour le paramètre UserInfo je ne sais pas quoi mettre comme Type de <T>! C'est juste une structure qui peut être USER_INFO1,2,3 etc. C'est pas un IEnumerable, une liste ou autre, juste une structure qui est différente!

    jeudi 7 juin 2012 13:38
  • Le type sera donc précisé à l'appel de la fonction UserAdd. Par contre dans le code cela devrait sans doute être <T> UserInfo ? (je ne sais pas ce qu'est FIXME<T> ?) et on doit donc pouvoir passer directement UserInfo sans passer par des pointeurs.

    A l'appel de USER_INFO_1 data;UserAdd(null,data) :

    - le type de la variable data est suffisant pour voir que pour cet appel <T> est USER_INFO_1 (UserAdd<USER_INFO_1>(null,data) marche mais est superflu).
    - dans UserAdd, on va donc récupérer le "level" associé à USER_INFO_1 qui est sans doute 1
    - on passe bien une structure correspondante de type USER_INFO_1 à l'API qui est ce qu'elle attend dans ces conditions

    Je note en tout cas ces APIs qui ont l'air de proposer des cas de figure intéressants pour creuser le marshalling. Dans le cas précédent on est passé par des pointeurs car la mémoire est allouée par la fonction de l'API et on doit refaire un appel après avec ce pointeur pour libérer la mémoire (donc on a probablement vraiment besoin du pointer). A priori ce n'est pas le cas ici (la fonction utilise juste les variables que l'on passe).


    Please always mark whatever response solved your issue so that the thread is properly marked as "Answered".

    jeudi 7 juin 2012 16:49
    Modérateur
  • En fait, mon problème

    c'est que je déclare ça:

    public static void UserAdd<T>(string ServerName, ref <T> UserInfo)
    et j'ai des tonnes d'erreurs de syntaxe sur cette ligne

    j'ai du mal à comprendre pourquoi!

    Type Attendu

    Erreur    3    Jeton ')' non valide dans la déclaration de membres de la classe, de la structure ou de l'interface


    Je suppose qu'il faut mettre quelque chose devant <T> mais quoi?



    jeudi 7 juin 2012 20:20
  •         public static void UserAdd<T>(string ServerName, ref T UserInfo)
            {
                uint Status;
                uint ParmErr = 0;
                IntPtr UserInfoPtr = IntPtr.Zero;
    
                Marshal.StructureToPtr(UserInfo, UserInfoPtr, true);
                Status = NetUserAdd(ServerName, UserInfoLevels[typeof(T)], UserInfoPtr, out ParmErr);
            }

    Ok le T passe sans <>, mais je sais pas si c'est la bonne manière de déclarer, pareil

    pour le UserAdd<T> du coup!

    Quand j'appelle la fonction, j'ai une jolie exception sur Marshal.StructureToPtr() :-)



    jeudi 7 juin 2012 20:34
  • Au final j'ai une version qui fonctionne.

    Si quelqu'un voit des erreurs ou des améliorations à apporter je suis preneur ;)

            public static void UserAdd<T>(string ServerName, ref T UserInfo)
            {
                uint NetStatusApi;
                uint ParmErr;
                IntPtr UserInfoPtr;

                UserInfoPtr = Marshal.AllocHGlobal(Marshal.SizeOf(UserInfo));

                try
                {
                    Marshal.StructureToPtr(UserInfo, UserInfoPtr, false);
                    NetStatusApi = NetUserAdd(ServerName, UserInfoLevels[typeof(T)], UserInfoPtr, out ParmErr);
                    if (NetStatusApi != NERR_Success)
                    {
                        throw new Win32Exception(Marshal.GetLastWin32Error());
                    }
                }
                finally
                {
                    Marshal.FreeHGlobal(UserInfoPtr);
                }
            }




    jeudi 7 juin 2012 22:23
  • Donc pour moi on devrait pouvoir se passer du pointeur en passant directement UserInfo. J'essaierai de faire un petit article sur le sujet si j'ai le temps...


    Please always mark whatever response solved your issue so that the thread is properly marked as "Answered".

    vendredi 8 juin 2012 09:32
    Modérateur
  •  Je vois pas comment faire pour passer directement,

    Si je reprends la fonction ça devrait être:

            public static void UserAdd<T>(string ServerName, ref T UserInfo)
            {
                uint NetStatusApi;
                uint ParmErr;
    
                NetStatusApi = NetUserAdd(ServerName, UserInfoLevels[typeof(T)], ref UserInfo, out ParmErr);
                if (NetStatusApi != NERR_Success)
                    throw new Win32Exception(Marshal.GetLastWin32Error());
            }


    Par contre j'arrive pas à prototyper la fonction de l'API

    [DllImport("netapi32.dll", EntryPoint = "NetUserAdd", CharSet = CharSet.Unicode, SetLastError = true)]
            public static extern uint NetUserAdd(
                [In] [MarshalAsAttribute(UnmanagedType.LPWStr)] string servername,
                [In] uint level,
    [In] ref T buf, // Fonctionne pas marque,  Le type ou le nom d'espace de noms 'T' est introuvable (pareil avec <T>)
                [Out] out uint parm_err
                );
    

    En gros je vois pas comment mettre une référence générique là? ça foire :) Sûrement des trucs qui m'échappent encore :-)

    Merci bcp Patrice pour toutes les bonnes idées et solutions que tu as apportées, le code que j'ai est déjà bien plus beau que ma première ébauche!!

             
    vendredi 8 juin 2012 10:51
  • Cela devrait pourtant marcher avec <T> ?

    [DllImport("netapi32.dll", EntryPoint = "NetUserAdd", CharSet = CharSet.Unicode, SetLastError = true)]
           
    public static extern uint NetUserAdd<T>(
               
    [In] [MarshalAsAttribute(UnmanagedType.LPWStr)] string servername,
               
    [In] uint level,
    [In] ref T buf, // Fonctionne pas marque,  Le type ou le nom d'espace de noms 'T' est introuvable (pareil avec <T>)
               
    [Out] out uint parm_err
               
    );

    Cela compile chez moi mais pas le temps de tester concrètement. Si tu avais mis le <T> au niveau du "ref <T> buf", le principe est en gros que <> après le nom de la fonction indique une liste des types qui servent de "paramètres" à la fonction exactement comme () entoure les "vrais" paramètres. Et dans le corps de la fonction, on trouve directement le type donc T et non pas <T>.

    Avec cette définition on devrait donc pouvoir appeler UserAdd("serveur",data) data étant de type USER_INFO_1 et donc déclencher un appel à la fonction de l'API avec une variable data de type USER_INFO_1 directement sans passer par des pointeurs.

    Je crois voir aussi que chaque structure USER_INFO_X hérite de la précédente donc ici on devrait pouvoir faire des classes qui héritent l'une de l'autre et mettre une contrainte sur le type T pour indiquer qu'il doit nécessairement hériter de USER_INFO_1.

    Eventuellement je ferais un essai avec NetServerGetInfo qui semble avoir la même structure et sera moins gênant pour faire des essais que de créer des comptes...


    Please always mark whatever response solved your issue so that the thread is properly marked as "Answered".

    vendredi 8 juin 2012 11:23
    Modérateur
  • Chez moi j'ai rajouté <T>, ça compile par contre au lancement j'ai une super exception:

    Exception non gérée : System.TypeLoadException: Une méthode générique ou une méthode dans une classe générique est un appel interne, PInvoke ou est définie dans une classe d'importation COM.

    Je suis pas sûr qu'il aime le <T> à une fonction DllImportée, mais ça vient peut être de moi qui ai fait une boulette ailleurs :-)


    vendredi 8 juin 2012 11:44
  • Sinon avec la vieille méthode en espérant pas faire trop d'erreurs (ça fonctionne chez moi):

            [DllImport("netapi32.dll", EntryPoint = "NetUserGetInfo", CharSet = CharSet.Unicode, SetLastError = true)]
            public static extern uint NetUserGetInfo(
                [In] [MarshalAsAttribute(UnmanagedType.LPWStr)] string servername,
                [In] [MarshalAsAttribute(UnmanagedType.LPWStr)] string username,
                [In] uint level,
                [Out] out IntPtr bufptr
                );

            public static T UserGetInfo<T>(string ServerName, string UserName)
            {
                uint NetStatusApi;
                IntPtr UserInfoPtr;
                T UserInfo;
    
                NetStatusApi = NetUserGetInfo(ServerName, UserName, UserInfoLevels[typeof(T)], out UserInfoPtr);
                if (NetStatusApi != NERR_Success)
                        throw new Win32Exception(Marshal.GetLastWin32Error());
    
                UserInfo = (T)Marshal.PtrToStructure(UserInfoPtr, typeof(T));
                if (UserInfoPtr != IntPtr.Zero)
                    NetApiBufferFree(UserInfoPtr);
    
                return UserInfo;
            }

    vendredi 8 juin 2012 12:39
  • Au temps pour moi la doc http://msdn.microsoft.com/fr-fr/library/system.runtime.interopservices.dllimportattribute.aspx indique effectivement que DllImport ne prends pas en charge les types génériques. Je pense que le mieux est d'en rester là pour l'instant. Je posterai un lien ici si je fais un test et si je trouve qq chose... DllImport se base peut-être seulement sur la signature de la fonction ce qui exclut les types génériques et l'héritage...


    Please always mark whatever response solved your issue so that the thread is properly marked as "Answered".


    vendredi 8 juin 2012 12:40
    Modérateur
  • Voilà j'ai refait la fonction UserEnum à ma sauce, mais je sais pas si c'est une bonne idée. :-)

    En gros il se peut en appellant NetUserEnum qu'on recoive un ERROR_MORE_DATA qui dit qu'il y'a + de données à fetcher. Du coup je fais un do/while et je renvoie une List<T> (qui je crois est aussi IEnumerable) et dont on a pas besoin de gérer le resize (je crois).

    Je me suis inspiré du code trouvé ici: http://msdn.microsoft.com/en-us/library/windows/desktop/aa370652%28v=vs.85%29.aspx

    Il me reste toute une série de questions pour les plus motivés que j'ai mis dans le code ;-)

            public static List<T> UserEnum<T>(string ServerName, uint Filter = 0)
            {
                uint NetStatusApi;
                IntPtr BufPtr;
                uint PrefMaxLen = 65535;
                uint EntriesRead;
                uint TotalEntries;
                uint ResumeHandle = 0;  // 1. Est ce vraiment un uint ou il faut mettre un IntPtr et faire quelque chose dessus?
                List<T> UserInfos = new List<T>();
    
                do
                {
                    NetStatusApi = NetUserEnum(ServerName, UserInfoLevels[typeof(T)], Filter, out BufPtr, PrefMaxLen, out EntriesRead, out TotalEntries, ref ResumeHandle);
    
                    try
                    {
                        if ((NetStatusApi == NERR_Success) || (NetStatusApi == ERROR_MORE_DATA))
                        {
                            var IterPtr = BufPtr;
    
                            if (IterPtr != IntPtr.Zero) // 2. Est ce vraiment nécessaire de tester ça?
                                for (int i = 0; i < EntriesRead; i++)
                                {
                                    UserInfos.Add((T)Marshal.PtrToStructure(IterPtr, typeof(T))); // 3. Faut il copier les données dans la liste à cause de problèmes mémoires possibles?
                                    IterPtr += Marshal.SizeOf(typeof(T));
                                }
                        }
                        else
                        {
                            throw new Win32Exception(Marshal.GetLastWin32Error());
                        }
                    }
                    finally // 4. Est ce que c'est correct de faire un try/finally, juste pour faire un free dans tous les cas?
                    {
                        if (BufPtr != IntPtr.Zero)
                            NetApiBufferFree(BufPtr);
                    }
    
                }
                while (NetStatusApi == ERROR_MORE_DATA);
    
                return UserInfos;
            }

    Voilà (ça fonctionne chez moi mais comme je débute vraiment depuis 2 semaines le c# j'ai peur de faire des grosses bétises d'où les questions).




    vendredi 8 juin 2012 13:59
  • Je vais tenter de répondre moi même à mes remarques dans le code :-)

    1. Dans la WinAPI, on retrouve le resume_handle avec la déclaration suivante, en gros un pointeur sur un unsigned int si je ne m'abuse.

    __inout  LPDWORD resume_handle (avec resume_handle = 0 au premier appel)

    En gros j'utilise une reference sur un uint, donc pour moi c'est correct pas besoin de mettre un IntPtr.

    2. Ca sert à rien de tester ce pointeur car si EntriesRead n'est pas nul et qu'on rentre dans notre fonction on peut penser que NetUserEnum ne fait pas pointer IterPtr sur une valeur nulle...

    3. Je ne pense pas qu'il faille copier les valeurs, en gros le code non managé (NetUserEnum) va allouer un espace mémoire (BufPtr) pour les structures. On va parcourir ces structures et rajouter leurs adresses dans une liste et dans du code managé. Même si on free l'espace allouée par NetUserEnum, je suppose que c'est l'espace mémoire du code non managée qui est free et que nos pointeurs sont donc valides ? 

    4. Je sais pas trop si c'est la bonne manière, ça me plait pas trop. Mais comme j'aimerai générer une exception quand le code de retour de NetUserEnum n'est pas bon ... je voyais que cette manière de faire.

    Il existe aussi celle là, mais je sais pas si elle est plus "propre":

    if ((NetStatusApi == NERR_Success) || (NetStatusApi == ERROR_MORE_DATA))
                        {
                            var IterPtr = BufPtr;
    
                            if (IterPtr != IntPtr.Zero) // 2. Est ce vraiment nécessaire de tester ça?
                                for (int i = 0; i < EntriesRead; i++)
                                {
                                    UserInfos.Add((T)Marshal.PtrToStructure(IterPtr, typeof(T))); // 3. Faut il copier les données dans la liste à cause de problèmes mémoires possibles?
                                    IterPtr += Marshal.SizeOf(typeof(T));
                                }
    			if (BufPtr != IntPtr.Zero)
    			    NetApiBufferFree(BufPtr);
                        }
                        else
                        {
    			if (BufPtr != IntPtr.Zero)
    			    NetApiBufferFree(BufPtr);
                            throw new Win32Exception(Marshal.GetLastWin32Error());
                        }



    samedi 9 juin 2012 13:15
  • 1) oui

    2) je pense aussi que c'est le cas. Si la fonction dit qu'elle a retourné n entréesn, le pointeur est censé pointé sur n entrées

    3) L'appel à l'API a alloué de la mémoire non managée pour nous retourner les données. On récupère le pointeur et on copie les données en mémoire managée puis on libère la mémoire non managée dont on a plus besoin. On pourrait garder les données retounées autant que l'on veut étant données que l'on en a fait une copie.

    4) D'après la doc si on utilise MAX_PREFERRED_LENGTH la fonction DOIT nécessairement retourner toutes les données d'un coup. Donc je pense que l'on peut ne pas gérer MORE_DATA (on peut mettre un test et générer une erreur par sécurité ou cas où cela arriverait tout de même).


    Please always mark whatever response solved your issue so that the thread is properly marked as "Answered".

    samedi 9 juin 2012 20:04
    Modérateur
  • Pour la 3)

    UserInfos.Add((T)Marshal.PtrToStructure(IterPtr, typeof(T)));

    Le Add d'une liste copie la donnée ou juste l'adresse du pointeur ? La donnée?

    Moi je pensais qu'en fait il stockait les pointeurs dans un espace d'adressage différent que le code unmanagé, et c'est pour ça qu'on pouvait faire un free de la zone mémoire unmanagé. Parce que sinon problème

    Pour la 4)

    Ca peut quand même arriver si on a beaucoup d'utilisateurs/structures à parcourir. Sait-on jamais, c'est pas impossible d'avoir 3000 utilisateurs ;-).

    Soit on alloue un max en mettant MAX_PREFERRED_LENGTH à sa valeur (-1)DWORD mais en c# ça fait un max de mémoire (4Go?)... A mon avis faut mieux faire à coup de 65535 mais avec un do while :-)


    samedi 9 juin 2012 21:45
  • PtrToStructure copie les données d'un emplacement non managé vers une structure ou une classe managée. On peut donc utiliser la valeur de retour comme bon non semble en code managée, puis libérer la mémoire non managée, les données ayant été copiées. Add ne joue aucun role dans ce mécanisme, cela fonctionne juste comme sur n'importe quel aure objet managé. C'est PtrToStructure qui fait la transition.

    Ma compréhension est que la valeur -1 est reconnue spécifiquement et ne pas va allouer la totalité de la mémoire correspondante mais autant que nécessaire pour stocker la totalité des données (à voir x utilisateurs x y octets en moyenne, à priori ce ne va pas chercher bien loin même pour beaucoup d'utilisateurs) ce qui semblait être la stratégie d'origine.

    Si effectivement on passe de -1 à 65535 on peut effectivement faire un do {} while (status==MORE_DATA) pour lire les données tant qu'il y a des encore des données...


    Please always mark whatever response solved your issue so that the thread is properly marked as "Answered".

    • Marqué comme réponse SylvainnnD lundi 11 juin 2012 11:31
    lundi 11 juin 2012 08:37
    Modérateur
  • Super Patrice,

    Pour le coup je crois qu'on est allé bien en profondeur sur le sujet et que tu as répondu à toutes mes questions! :)

    Bravo et Merci!

    lundi 11 juin 2012 11:32