IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)
Navigation

Inscrivez-vous gratuitement
pour pouvoir participer, suivre les réponses en temps réel, voter pour les messages, poser vos propres questions et recevoir la newsletter

Delphi Discussion :

Fonction dans une DLL recevant et renvoyant une chaîne


Sujet :

Delphi

  1. #1
    Rédacteur/Modérateur

    Avatar de Roland Chastain
    Homme Profil pro
    Enseignant
    Inscrit en
    Décembre 2011
    Messages
    4 085
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 51
    Localisation : France, Moselle (Lorraine)

    Informations professionnelles :
    Activité : Enseignant

    Informations forums :
    Inscription : Décembre 2011
    Messages : 4 085
    Points : 15 492
    Points
    15 492
    Billets dans le blog
    9
    Par défaut Fonction dans une DLL recevant et renvoyant une chaîne
    Bonjour !

    J'ai une unité que j'ai voulu transformer en DLL et apparemment je m'y suis mal pris puisque le programme utilisant la DLL cesse de fonctionner au moment de se fermer. (Plus exactement, l'une des deux versions de ce programme, l'autre paraissant fonctionner normalement.) La fonction appelée reçoit comme paramètre une chaîne (entre autres) et renvoie une chaîne. J'ai fait quelques recherches sur le sujet et j'ai cru comprendre que c'est de là que venait le problème, mais je n'ai pas trouvé d'exemple qui colle exactement à mon cas.
    Je voudrais, si possible, que ma DLL soit également compilable et utilisable avec Free Pascal. Pourriez-vous me mettre sur la voie de la solution la plus universelle, c'est-à-dire qui puisse fonctionner avec différents compilateurs, et éventuellement même me permettre d'utiliser ma DLL depuis un programme en C (si possible).
    Voici l'en-tête de ma fonction. Je mets dans une pièce jointe tous les autres fichiers, y compris le résultat de mes essais avec différents compilateurs (Delphi 7, Delphi XE2, Free Pascal). Pour résumer, le chargement statique (j'espère ne pas dire de bêtise) fonctionne. C'est avec le chargement dynamique que l'erreur se produit. Par contre, avec Free Pascal, les deux versions plantent en cours de route. Mais si j'utilise directement l'unité qui est "derrière" la DLL, tout fonctionne correctement avec les trois compilateurs.

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    library Matador;
     
    {$IFDEF FPC}{$MODE DELPHI}{$ENDIF}
    {$WARN SYMBOL_PLATFORM OFF}
     
    uses
      SysUtils, MatadorCore;
     
    function SolveMate(
      const aFen: string;
      const aMovesNumber: integer;
      const aSearchAllMoves: boolean
    ): string; export;

  2. #2
    Expert éminent sénior
    Avatar de Paul TOTH
    Homme Profil pro
    Freelance
    Inscrit en
    Novembre 2002
    Messages
    8 964
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 55
    Localisation : France, Paris (Île de France)

    Informations professionnelles :
    Activité : Freelance
    Secteur : High Tech - Éditeur de logiciels

    Informations forums :
    Inscription : Novembre 2002
    Messages : 8 964
    Points : 28 457
    Points
    28 457
    Par défaut
    tu peux lire ce fil sur le problème que tu rencontres.

    mais pour faire court, remplace tes String pas de PChar et tu seras compatible FreePascal et même tout autre lange de programmation. Le type String de Delphi implique l'usage du gestionnaire de mémoire de celui qui la crée, ce n'est donc pas une bonne idée de transmettre des Strings à une DLL

  3. #3
    Invité
    Invité(e)
    Par défaut
    Bonjour,

    je n'ai pas examiné votre code (question de temps ) mais d'une manière générale, je ne comprends pas* (plus) l'intérêt de créer une dll en Delphi/Lazarus pour l'utiliser... en Delphi/Lazarus. Ne serait-ce que pour simplifier les problèmes de compilation, la création et l'utilisation d'un composant à la place d'une dll sont nettement plus performantes. Les facilités de ré-employabilité et la réalisation d'éventuelles corrections immédiates de cette pseudo "dll" (i.e. le composant) directement à partir du projet, sous n'importe quel OS, sont d'un confort et d'une "productivité" (dirait Serge) incomparables... Je parle de Lazarus parce que je suis beaucoup moins convaincu par Delphi FMX (et pas du tout par Qt à ce niveau, parce qu'en ce domaine, la création et l'utilisation de composants personnels sont dans les faits quasiment inenvisageables)

    Par contre (et pour rendre ce message un peu plus productif), j'utilise assez fréquemment en Windev des dll créées en Lazarus (avec source de 3 méthodes Lazarus appelées par Windev). On utilise également des pChar pour passer des chaînes. Entre 2 langages quand c'est possible (par exemple utiliser des bibliothèques C++ en Lazarus ne l'est pas... et c'est bien dommage !), cela présente en effet un intérêt. Mais quelle lourdeur !

    En Lazarus attention à la déclaration des méthodes : stdcall vs cdecl.


    *Il est vrai que si vous êtes sous Delphi, je considère que la facilité de mise en oeuvre des composants est nettement inférieure à celle de Lazarus. Alors peut-être que la dll est plus pratique à mettre en oeuvre au départ...
    Dernière modification par Invité ; 19/04/2015 à 11h40.

  4. #4
    Rédacteur/Modérateur

    Avatar de Roland Chastain
    Homme Profil pro
    Enseignant
    Inscrit en
    Décembre 2011
    Messages
    4 085
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 51
    Localisation : France, Moselle (Lorraine)

    Informations professionnelles :
    Activité : Enseignant

    Informations forums :
    Inscription : Décembre 2011
    Messages : 4 085
    Points : 15 492
    Points
    15 492
    Billets dans le blog
    9
    Par défaut
    Merci pour vos réponses. Je vais donc regarder du côté des PChars.

    @selzig

    Merci pour le lien vers les exemples. A plus tard pour la suite de la discussion !

  5. #5
    Expert confirmé
    Avatar de Ph. B.
    Homme Profil pro
    Freelance
    Inscrit en
    Avril 2002
    Messages
    1 786
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 58
    Localisation : France, Haute Garonne (Midi Pyrénées)

    Informations professionnelles :
    Activité : Freelance
    Secteur : High Tech - Éditeur de logiciels

    Informations forums :
    Inscription : Avril 2002
    Messages : 1 786
    Points : 5 918
    Points
    5 918
    Par défaut
    Bonjour,
    Citation Envoyé par selzig Voir le message
    *Il est vrai que si vous êtes sous Delphi, je considère que la facilité de mise en oeuvre des composants est nettement inférieure à celle de Lazarus. Alors peut-être que la dll est plus pratique à mettre en oeuvre au départ...

    Y aurait-il un contresens dans cette phrase ?
    Utilisant les 2 environnements, passer d'un système de composants liés dynamiquement (Delphi) à un système de composants liés statiquement (Lazarus) est AMHA un inconvénient sérieux.

  6. #6
    Invité
    Invité(e)
    Par défaut
    Bonjour,

    Non, non, aucun contresens. Je ne parle pas de compilation mais de mise en oeuvre, de simplicité (d'environnement et de réalisation). Tout est dans les détails, certes. Mais les Lazarusiens sont très forts à ce niveau. C'est à peu près du même acabit que les ancrages des objets graphiques sur une Form. Quand on revient à Delphi, on trouve leur choix "inapproprié", lourd et maladroit... Quoiqu'il en soit, je fais avec. Il est vrai que ma référence à Lazarus est maladroite : les composants sont tout à fait (facilement) réalisables avec Delphi (y compris FMX), c'est une des premières choses que j'ai vérifiée en "reprenant" Delphi.

    D'un autre côté, hormis pour le déploiement, où je privilégie la compilation statique (indispensable à mon avis en multi-OS), les 2 choix ont respectivement leurs avantages et leurs inconvénients. Et que ce soit Delphi et Lazarus, compiler un projet est extrêmement rapide (et simple).
    Dernière modification par Invité ; 19/04/2015 à 17h47.

  7. #7
    Rédacteur/Modérateur

    Avatar de Roland Chastain
    Homme Profil pro
    Enseignant
    Inscrit en
    Décembre 2011
    Messages
    4 085
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 51
    Localisation : France, Moselle (Lorraine)

    Informations professionnelles :
    Activité : Enseignant

    Informations forums :
    Inscription : Décembre 2011
    Messages : 4 085
    Points : 15 492
    Points
    15 492
    Billets dans le blog
    9
    Par défaut
    Donc j'ai essayé de remplacé les strings par des pchars (ou plutôt par des pansichars, je pense que c'est le meilleur choix dans ce cas, non ?) et tout semble fonctionner. Enfin, tout fonctionne avec Delphi 7 et Free Pascal. Avec XE2, ça ne plante plus mais ça ne renvoie plus les bons résultats. Je suppose qu'il faut que je mette des ansistrings partout, mais j'aurais aimé avoir votre avis avant de me lancer à tout modifier.

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    library Matador;
     
    {$WARN SYMBOL_PLATFORM OFF}
    {$IFDEF FPC}{$MODE DELPHI}{$ENDIF}
     
    uses
      SysUtils, MatadorCore;
     
    function SolveMate(
      //const aFen: string;
      const aFen: pansichar;
      const aMovesNumber: integer;
      const aSearchAllMoves: boolean
    //): string; export;
    ): pansichar; export;
    var
      lMoveDepth: integer;
      lTurn: integer;
      lMove: TMoveRecord;
      lFen: ansistring;
    begin
      //result := '';
      result := #0;
      lFen := aFen;
     
      //if InitializeGlobalPosition(aFen, lTurn) then
      if InitializeGlobalPosition(lFen, lTurn) then
      begin
        gNodes := 0;
     
        if SearchMate(
          lTurn,
          1,
          aMovesNumber,
          lMoveDepth,
          lMove,
          not aSearchAllMoves
        ) then
          //result := Concat(SquareToStr(lMove.sqFrom), SquareToStr(lMove.sqTo));
          result := pansichar(Concat(SquareToStr(lMove.sqFrom), SquareToStr(lMove.sqTo)));
      end;
    end;
    Remarquez que je n'emploie ni le mot-clé stdcall ni cdecl, mais simplement export : c'est comme ça qu'était fait l'exemple de DLL dont j'étais parti.

    Autre chose, toujours dans l'idée de pouvoir utiliser ma DLL dans un programme écrit dans un autre langage (en C par exemple), ne faudrait-il pas remplacer l'argument de type boolean par, mettons, un argument de type integer ? Qu'en pensez-vous ?

  8. #8
    Modérateur
    Avatar de tourlourou
    Homme Profil pro
    Biologiste ; Progr(amateur)
    Inscrit en
    Mars 2005
    Messages
    3 877
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 61
    Localisation : France, Yvelines (Île de France)

    Informations professionnelles :
    Activité : Biologiste ; Progr(amateur)

    Informations forums :
    Inscription : Mars 2005
    Messages : 3 877
    Points : 11 373
    Points
    11 373
    Billets dans le blog
    6
    Par défaut
    Bonsoir Roland,

    Si la librairie doit être utilisée avec un autre langage, autant fixer une convention explicite pour le passage des paramètres : la traduction sera plus sûre pour les utilisateurs non delphistes.

    Autant aussi restreindre les types à ceux "universels" : pourquoi en effet ne pas remplacer aSearchAllMoves: boolean par aSearchOptions: integer où aSearchOptions pourra au besoin combiner plusieurs options, chacune selon un masque ?

    Quant au résultat, pourquoi serait-il erroné en passant de string à PAnsiChar result := pansichar(Concat(SquareToStr(lMove.sqFrom), SquareToStr(lMove.sqTo)));, et seulement sous XE2, si l'entrée du problème est correcte en PAnsiChar ?

  9. #9
    Rédacteur/Modérateur

    Avatar de Roland Chastain
    Homme Profil pro
    Enseignant
    Inscrit en
    Décembre 2011
    Messages
    4 085
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 51
    Localisation : France, Moselle (Lorraine)

    Informations professionnelles :
    Activité : Enseignant

    Informations forums :
    Inscription : Décembre 2011
    Messages : 4 085
    Points : 15 492
    Points
    15 492
    Billets dans le blog
    9
    Par défaut
    @tourlourou

    Bonsoir et merci pour ta réponse.

    Citation Envoyé par tourlourou Voir le message
    Si la librairie doit être utilisée avec un autre langage, autant fixer une convention explicite pour le passage des paramètres : la traduction sera plus sûre pour les utilisateurs non delphistes.
    D'accord. Donc je pense que ce sera stdcall, puisque j'ai lu que c'était la convention la plus répandue sous Windows.

    Citation Envoyé par tourlourou Voir le message
    Autant aussi restreindre les types à ceux "universels" : pourquoi en effet ne pas remplacer aSearchAllMoves: boolean par aSearchOptions: integer où aSearchOptions pourra au besoin combiner plusieurs options, chacune selon un masque ?
    Oui, bonne idée.

    Citation Envoyé par tourlourou Voir le message
    Quant au résultat, pourquoi serait-il erroné en passant de string à PAnsiChar result := pansichar(Concat(SquareToStr(lMove.sqFrom), SquareToStr(lMove.sqTo)));, et seulement sous XE2, si l'entrée du problème est correcte en PAnsiChar ?
    C'est un fait que le résultat est faux. Comme j'ai modifié l'en-tête de la fonction principale (celle qui est exportée) sans changer le corps du code, où c'est toujours le type string qui est utilisé, je me suis dit que c'était probablement à cause de cela que la fonction renvoyait un résultat faux lorsque la DLL est compilée avec XE2. D'ailleurs à la compilation j'ai des avertissements du genre "transtypage suspect". Je vais essayer de mettre des ansistrings partout pour voir ce que ça donne. De toute façon le programme n'est pas bien gros.

  10. #10
    Rédacteur/Modérateur

    Avatar de Roland Chastain
    Homme Profil pro
    Enseignant
    Inscrit en
    Décembre 2011
    Messages
    4 085
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 51
    Localisation : France, Moselle (Lorraine)

    Informations professionnelles :
    Activité : Enseignant

    Informations forums :
    Inscription : Décembre 2011
    Messages : 4 085
    Points : 15 492
    Points
    15 492
    Billets dans le blog
    9
    Par défaut
    Voilà, tout fonctionne correctement avec les trois compilateurs ! Merci pour votre aide.

    Il ne me reste qu'à remplacer l'argument booléen, pour voir ensuite si j'arrive à utiliser la DLL dans un autre langage. La suite demain.

  11. #11
    Expert éminent sénior
    Avatar de Paul TOTH
    Homme Profil pro
    Freelance
    Inscrit en
    Novembre 2002
    Messages
    8 964
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 55
    Localisation : France, Paris (Île de France)

    Informations professionnelles :
    Activité : Freelance
    Secteur : High Tech - Éditeur de logiciels

    Informations forums :
    Inscription : Novembre 2002
    Messages : 8 964
    Points : 28 457
    Points
    28 457
    Par défaut
    il reste un soucis dans ton code, c'est expliqué dans le chapitre sur les DLL du livre Delphi 7 Studio.

    prenons un exemple:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
     
    function Test: PAnsiChar;
    var
      s: string;
    begin
      s := "un" + IntToStr(2);
      Result := PAnsiChar(s);
    end;
    dans cette fonction la variable s est locale, elle est allouée par la fonction (il faut quelle soit calculée sinon c'est une simplement chaîne constante, c'est important). A la sortie de la fonction elle sera libérée automatiquement par Delphi.

    Mais qu'en est-il du PAnsiChar ? qui va le libérer si toutefois il reste alloué (ce dont je doute).

    il y a deux méthodes traditionnellement utilisée pour gérer cela:

    1) la DLL propose une fonction de libération du PChar renvoyé (c'est rare sous Windows)

    2) la DLL attend en paramètre un buffer qui doit recevoir la donnée, elle n'est donc pas responsable de l'allocation de celui-ci, ni de sa libération

    exemple GetUserName().

    il existe aussi une troisième approche utilisée notamment par inet_ntoa, elle consiste à retourner une chaîne dont la valeur n'est garantie que jusqu'au prochain appel à la DLL, l'explication en est simple avec le code ci-dessous

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
     
    var
      ReturnString: AnsiString;
     
    function Test: AnsiString; // NB: le code de la DLL peux utiliser un AnsiString (qui est un pointeur), mais l'exécutable doit faire appel à une fonction qui retourne un PAnsiChar, sinon Delphi risque de vouloir gérer la String
    begin
      Returnstring := "un" + IntToStr(2);
      Result := ReturnString;
    end;
     
    function Test2: AnsiString;
    begin
      Returnstring := "deux" + IntToStr(3);
      Result := ReturnString;
    end;
    ici, la DLL conserve une copie de la chaîne dans une variable globale (qui n'est donc pas libérée en sortie de fonction), sa valeur sera remplacée automatiquement lors du prochain appel à la DLL, de son côté le programme n'a qu'une contrainte, c'est de faire une copie de la valeur du PAnsiChar retourné avant de faire à nouveau appel à la DLL. Si l'application est développée sous Delphi, il suffit donc de placer le PAnsiChar dans un String et c'est fini.

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
     
    function Test():PAnsiChar; external 'madll.dll'; // c'est bien un PAnsiChar ici (qui lui aussi est un pointer) mais on ne veux pas que Delphi y touche
    function Test2():PAnsiChar; external 'madll.dll'; 
     
    var
      s1, s2: string;
    begin
      s1 := Test();   // Copie le PAnsiChar dans s1
      s2 := Test2(); // Copie le PAnsiChar dans s2, s1 et s2 sont des valeurs locales
    end;
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
     
    var
      p1, p2: PAnsiChar;
    begin
      p1 := Test();   // pointe sur ResultString dans la DLL
      p2 := Test2(); // pointe sur la nouvelle ResultString dans al DLL, p1 N'EST PLUS VALIDE !
    end;

  12. #12
    Rédacteur/Modérateur

    Avatar de Roland Chastain
    Homme Profil pro
    Enseignant
    Inscrit en
    Décembre 2011
    Messages
    4 085
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 51
    Localisation : France, Moselle (Lorraine)

    Informations professionnelles :
    Activité : Enseignant

    Informations forums :
    Inscription : Décembre 2011
    Messages : 4 085
    Points : 15 492
    Points
    15 492
    Billets dans le blog
    9
    Par défaut
    Merci Paul.

    Je me suis intéressé à la troisième approche, qui m'a paru la plus simple, mais... je n'arrive pas à faire fonctionner ton exemple.

  13. #13
    Expert éminent sénior
    Avatar de Paul TOTH
    Homme Profil pro
    Freelance
    Inscrit en
    Novembre 2002
    Messages
    8 964
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 55
    Localisation : France, Paris (Île de France)

    Informations professionnelles :
    Activité : Freelance
    Secteur : High Tech - Éditeur de logiciels

    Informations forums :
    Inscription : Novembre 2002
    Messages : 8 964
    Points : 28 457
    Points
    28 457
    Par défaut
    en effet...pas le temps de creuser le pourquoi, mais voici une solution

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
     
    var
      ReturnString: AnsiString;
     
    function Test: PAnsiChar; stdcall; // NB: le code de la DLL peux utiliser un AnsiString (qui est un pointeur), mais l'exécutable doit faire appel Ã* une fonction qui retourne un PAnsiChar, sinon Delphi risque de vouloir gérer la String
    begin
      ReturnString := 'un' + IntToStr(2);
      Result := Pointer(ReturnString);
    end;
     
    function Test2: PAnsiChar; stdcall;
    begin
      ReturnString := 'deux' + IntToStr(3);
      Result := Pointer(ReturnString);
    end;
    en fait il est possible qu'en retournant une AnsiString depuis une DLL Delphi fasse une copie de la chaîne au lieu de la retourner directement et on retombe dans le problème initial.

    je force "Pointer(ReturnString)" pour éviter d'atures effets de bords, mais le "Pointer" n'est pas forcément nécessaire...à tester.

  14. #14
    Rédacteur/Modérateur

    Avatar de Roland Chastain
    Homme Profil pro
    Enseignant
    Inscrit en
    Décembre 2011
    Messages
    4 085
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 51
    Localisation : France, Moselle (Lorraine)

    Informations professionnelles :
    Activité : Enseignant

    Informations forums :
    Inscription : Décembre 2011
    Messages : 4 085
    Points : 15 492
    Points
    15 492
    Billets dans le blog
    9
    Par défaut
    Merci, Paul. Cette fois, tout semble fonctionner parfaitement. Je joins le dernier état du projet, avec la DLL compilée et un exemple d'utilisation en Free Basic. Si jamais quelqu'un d'autre est intéressé par le sujet et veut proposer un exemple d'utilisation de la DLL dans un autre langage, je suis preneur.

  15. #15
    Rédacteur/Modérateur

    Avatar de Roland Chastain
    Homme Profil pro
    Enseignant
    Inscrit en
    Décembre 2011
    Messages
    4 085
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 51
    Localisation : France, Moselle (Lorraine)

    Informations professionnelles :
    Activité : Enseignant

    Informations forums :
    Inscription : Décembre 2011
    Messages : 4 085
    Points : 15 492
    Points
    15 492
    Billets dans le blog
    9
    Par défaut
    Voici un lien vers la dernière version du projet, incluant la DLL compilée, des exemples d'utilisation en Basic (Free Basic), C (Borland C++) et Pascal (Delphi, Free Pascal), et le programme original de Valentin Albillo.

    http://pascal.developpez.com/telecha...artie-d-echecs

+ Répondre à la discussion
Cette discussion est résolue.

Discussions similaires

  1. Renommer une fonction dans un select ou concevoir autrement une fonction
    Par tavarlindar dans le forum Général JavaScript
    Réponses: 16
    Dernier message: 30/05/2008, 17h17
  2. Réponses: 1
    Dernier message: 24/04/2007, 09h27
  3. Réponses: 1
    Dernier message: 22/09/2005, 15h46
  4. Trouver une fonctions dans des DLL
    Par Mercenary Developer dans le forum Langage
    Réponses: 2
    Dernier message: 08/09/2005, 15h28
  5. [DLL] utiliser une DLL a partir d' une DLL et un .def
    Par venomelektro dans le forum MFC
    Réponses: 9
    Dernier message: 07/12/2004, 14h01

Partager

Partager
  • Envoyer la discussion sur Viadeo
  • Envoyer la discussion sur Twitter
  • Envoyer la discussion sur Google
  • Envoyer la discussion sur Facebook
  • Envoyer la discussion sur Digg
  • Envoyer la discussion sur Delicious
  • Envoyer la discussion sur MySpace
  • Envoyer la discussion sur Yahoo