Baniere Saphir Control
http://www.saphir-control.fr/

bar


BACK

Tous ces articles en ligne sur Scilab ont été écrits pour LINUX MAGAZINE FRANCE par les développeurs du Scilab Group. Les articles sont sous licence FDL (Free Documentation Licence)

Scilab: Appel depuis C, Fortran, Java, ...

Jean-Philippe Chancelier
Cermics-Enpc
Scilab Group

25 Janvier 2001

1   Introduction

Nous allons voir dans cet article comment utiliser les fonctionalités de Scilab à partir d'une application externe. On voit donc Scilab comme une librairie que l'on va lier à notre application externe. Il est bien sur toujours possible d'oublier l'interprète Scilab et de voir Scilab comme une collection de fonctions C ou Fortran que l'on utilise directement. Ce ne sera pas notre point de vue ici. On suppose en effet que l'on ne connaît les fonctionnalités de Scilab qu'a travers le manuel en ligne et on ne veut pas <<fouiller>> dans le code Scilab pour trouver par exemple que la fonction rand de Scilab utilise de façon interne une routine Fortran urand. La fonction de base que l'on va utiliser sera en conséquence la fonction interne scifun qui permet de faire exécuter à Scilab des instructions décrites par une chaîne de caractères. Pour créer un vecteur aléatoire de taille 2 on pourra donc utiliser les instructions suivantes :
 
        strcpy(buf,"x=rand(1,2);");
        C2F(scifun)(buf,strlen(buf));
On verra dans la suite que l'on encapsulera l'appel de scifun pour pouvoir gérer les erreurs qui peuvent se produire en Scilab. On utilisera aussi des fonctions internes Scilab nous permettant d'accéder à des matrices ou des chaînes de caractères Scilab à partir de leurs noms. Par exemple la macro (définie dans routines/stack-c.h) :
 
        GetMatrixptr("X", &m, &n, &lp);
permet d'obtenir un pointeur vers les données associées à la matrice nommée X dans Scilab sous la forme stk(lp) et la taille de la matrice est obtenue dans (m,n). En cas d'echec GetMatrixptr effectue un return 0;. On pourra se reporter au CD du magazine pour obtenir le source complet des exemples que nous allons commenter. Le code se trouvera aussi dans le répertoire examples/callsci de la prochaine version de Scilab.

2   Quelques exemples

Dans notre premier exemple, on cherche à résoudre un système linéaire à savoir trouver x solution de Ax=b. La matrice A et le vecteur b sont codés en C au moyen de tableaux unidimensionnels. Les éléments Ai,j de la matrice sont stockés dans un tableau C au position A[in*j]+ ce qui correspond au stockage couramment utilisé dans les librairies numériques (et donc aussi dans Scilab). On envoie les deux matrices dans Scilab au moyen de la macro WriteMatrix (noter que cela implique une copie des objets), on effectue ensuite le calcul au moyen de send_scilab_job (dont nous verrons le code plus loin) et on récupère le résultat au moyen de GetMatrixptr ce qui se fait cette fois sans copie. Voila le code complet de cet exemple :
 
static int premier_exemple()
{
  static double A[]={1,2,3,4};  int mA=2,nA=2;
  static double b[]={4,5};  int mb=2,nb=1;
  int m,n,lp,i;
  WriteMatrix("A", &mA, &nA, A); 
  WriteMatrix("b", &mb, &nb, b);
        
  fprintf(stdout,"premier exemple\n");
  if ( send_scilab_job("A,b,x=A\\b;") != 0) 
    fprintf(stderr,"Erreur dans Scilab\n");
  else 
    {
      GetMatrixptr("x", &m, &n, &lp);
      for ( i=0 ; i < m*n ; i++) 
        fprintf(stdout,"x[%d] = %5.2f\n",i,*stk(i+lp));
    }
  return 1;
} 
L'éxecution de la routine précédente donnera l'affichage suivant :
 
 x[0] = -0.50
 x[1] =  1.50
et on vérifie bien (avec Scilab ou à la main !) que :
 
-->norm([1,3;2,4]*[-0.5;1.5]-[4;5])
 ans  =
 
    0.  
Dans un deuxième exemple on veut tracer une courbe à partir de la donnée de deux vecteurs (x,y). Plutôt que d'allouer les deux vecteurs dans le programme C on les <<alloue>> dans Scilab et on récupère des pointeurs vers les données. Cela nous évite d'avoir à effectuer des copies (WriteMatrix) pour transférer les données vers Scilab. Les tableaux sont ensuite remplis par notre programme C et la visualisation s'obtient grâce à la commande :
 
  send_scilab_job("plot(x,y);xclick();quit");
Dans cette commande on utilise xclick(). Ainsi le programme va attendre un <<click>> de l'utilisateur pour continuer son exécution. Pendant cette phase d'attente le gestionnaire d'événements de Scilab va être actif et les menus de la fenêtre graphique seront utilisables. Voila maintenant le code de ce deuxième exemple :
 
static int deuxieme_exemple() 
{
  int m,n,lx,ly,i;
  /* j'utilise scilab pour creer les abscisses 
   * et reserver de la place pour les ordonnees 
   */ 
  send_scilab_job("x=1:0.1:10;y=x;");
  GetMatrixptr("x", &m, &n, &lx);  
  GetMatrixptr("y", &m, &n, &ly);  
  for ( i=0; i < m*n ; i++) 
    { 
      double xi = *stk(lx+i);
      *stk(ly+i) = xi*sin(xi);
    }
  /* plot(x,y);  */
  send_scilab_job("plot(x,y);xclick();quit");
  return 1;
}
Dans ce dernier exemple on appelle l'intégrateur d'équations différentielles de Scilab (un article à déjà été consacré a ce sujet). L'équation différentielle est décrite en C et il faut donc en plus la charger dans l'environnement Scilab. Le code est sans surprise, on transmet à Scilab les conditions initiales et les temps pour lesquels on désire la solution au moyen de la macro WriteMatrix. On charge l'équation différentielle dans Scilab en utilisant la commande link de Scilab à travers un send_scilab_job et enfin on appelle l'intégrateur ode à nouveau au travers de la fonction send_scilab_job.
 
int troisieme_exemple() 
{
  double x[]={1,0,0} ; int mx=3,nx=1;
  double time[]={0.4,4}; int mt=1,nt=2;
  /* chargement du code de l'équation différentielle */
  send_scilab_job("link(''./mon_ode.o'',''mon_ode'',''c'');");
  /* transmission des arguments */
  WriteMatrix("x", &mx, &nx, x);
  WriteMatrix("time", &mt, &nt,time);
  /* appel de ode */
  send_scilab_job("y=ode(x,0,time,''mon_ode''),");
}
Le code de l'équation différentielle donné dans le fichier mon_ode.c est le suivant :
 
static double param[] ={0.04,10000,3.0e+7};

int mon_ode(neq, t, y, ydot)
     int *neq;
     double *t, *y, *ydot;
{
  ydot[0] = - param[0]*y[0] + param[1] * y[1] * y[2];
  ydot[2] =  param[2] * y[1] * y[1];
  ydot[1] = -ydot[0] - ydot[2];
  return 0;
}

3   Les ingrédients laissés en suspens

La fonction send_scilab_job(char *name) comme nous l'avons évoqué plus haut ne fait qu'encapsuler la fonction de base et son code est le suivant :
 
static int send_scilab_job(char *job) 
{
  static char buf[1024],
    format[]="Err=execstr('%s','errcatch','n');quit;";
  int m,n,lp;
  sprintf(buf,format,job);
  C2F(scirun)(buf,strlen(buf));
  GetMatrixptr("Err", &m, &n, &lp);
  return (int) *stk(lp);
}
Il n'y a donc rien de mystérieux la dedans, la fonction Scilab
Err=execstr(str,'errcatch','n')
permet de faire exécuter une instruction Scilab donnée par une chaîne de caractères tout en bloquant le mode de gestion des erreurs par défaut. En cas d'erreur aucun message n'est imprimé et la fonction execstr renvoie le code de l'erreur dans son argument de retour sinon la valeur retournée est zéro. L'instruction GetMatrixptr("Err",m,n,lp) nous permet de récupérer ce code d'erreur au niveau C. On notera que si les chaînes de caractères transmises à send_scilab_job contiennent le caractère <<'>> il faudra le doubler dans le code source, par exemple :
 
   send_scilab_job("exists(''x'')");
(on évitera d'utiliser le caractère <<">> car il faudrait alors le doubler pour Scilab et le protéger pour le C !). Il nous reste à écrire le programme principal qui va appeler les exemples précédents. Ce programme principal doit initialiser Scilab et avant de terminer il doit appeler la routine de sortie de Scilab sciquit. Comme certaines fonctions de Scilab sont en Fortran (Eh oui ! mais comme on dit <<old wine, old bottle>>) et qu'on les appelle à partir de C, on utilise une macro définie dans Scilab qui permet de gérer les différences de nommage entre C et Fortran. Ainsi sciquit sera appelée sous la forme C2F(sciquit)() dans du code C. L'initialisation de Scilab est résumée dans la fonction Initialize. On initialise Scilab et on fait exécuter le fichier de startup de Scilab. Le paramètre stacksize donne la taille de la pile Scilab que l'on demande en au départ. Dans notre version d'Initialize on fixe une valeur de la variable d'environnement SCI (Il y a une valeur par défaut ../../ qu'une option de compilation -DSCI=\"pathname\" peut écraser) pour pouvoir appeler notre exécutable directement sans avoir à fixer la variable SCI.
 
#include <math.h>
#include <stdio.h> 
#include "machine.h"
#include "stack-c.h"

#ifndef SCI 
#define SCI "../.."   /* Une valeur par defaut */
#endif 

static void Initialize() 
{
  static char initstr[]="exec(\"SCI/scilab.star\",-1);quit;";
  static iflag=-1, stacksize = 1000000, ierr=0;
  setenv("SCI",SCI,1);
  C2F(inisci)(&iflag,&stacksize,&ierr);
  if ( ierr > 0 ) 
    {
      fprintf(stderr,"Scilab initialization failed !\n");
      exit(1);
    }
  C2F(settmpdir)();
  C2F(scirun)(initstr,strlen(initstr));
}
Si on ne veut pas voir s'afficher la bannière de Scilab lors de l'initialisation, il suffit de redéfinir la fonction banier. Par exemple, de la façon suivante :
 
void C2F(banier)(int *x) 
{
  fprintf(stdout,"Ma banniere a moi\n");
  C2F(storeversion)("scilab-2.5.1",12L);
}
Reste le programme principal. On se rappelle à nouveau que Scilab contient une bonne part de code numérique Fortran et que lors de sa compilation il est plus simple d'obtenir l'exécutable final en appelant le compilateur Fortran (g77). Il se trouve que le runtime du Fortran fournit un programme main et que ce programme main attend de la part de l'utilisateur un point d'entrée nommé MAIN__ (disons que c'est au moins vrai pour g77). On écrit donc dans notre programme une fonction MAIN__ :
 
int MAIN__(void) 
{
  Initialize();
  premier_exemple();
  deuxieme_exemple();
  troisieme_exemple();
  C2F(sciquit)();
}

4   Compilation

Une fois le code écrit Il faut le compiler et le linker avec les librairies. Pour ce faire, nous nous sommes inspirés des fichiers du répertoire Scilab examples/callsci que nous avons organisé de façon légèrement différente. Pour adapter le code proposé, il suffit d'éditer le fichier Makefile et de renseigner les champs SCIDIR, PROG et OBJS. Puis un make distclean suivit de make all devrait effectuer la compilation et la création de l'exécutable PROG.
 

SCIDIR=/usr/local/lib/scilab-2.5.1

PROG=myprog 
OBJS=myprog.o 

#--------------------------------------------------------
# do not modify below 
#--------------------------------------------------------


all     : .init 
        make -f config/Makefile.all \
           PROG=$(PROG) OBJS="$(OBJS)" SCIDIR=$(SCIDIR)

.init   : Makefile 
        config/Init $(PROG) $(SCIDIR) 
        touch .init 

clean   : 
        rm -f $(PROG) $(OBJS)

distclean : 
        rm -f $(PROG) $(OBJS) $(PROG).sh .init config/Makefile 
        rm -f config/Makefile.Top
Le Makefile précédent commence par lancer un script Init dont le rôle est d'aller copier dans les répertoires Scilab les Makefile dont on a besoin pour linker les librairies Scilab à notre application. L'un des fichiers créé par Init est config/Makefile.incl. C'est une copie du fichier correspondant de Scilab et on pourra avoir besoin de l'éditer par exemple si on veut changer les options de compilation. Noter que les fichiers crées par Init ne sont recréés qu'après un make distclean. La compilation s'effectue en utilisant la commande make qui exécute Init que nous venons de voir puis qui lance un nouveau make du fichier config/Makefile.all. Le fichier config/Makefile.all qui préexiste dans le répertoire config utilise les information des fichiers créés et les variables PROG et OBJS pour effectuer la compilation. Pour conclure l'utilisateur n'a besoin de donner que la liste des fichiers à compiler et il n'a pas en principe à se préoccuper d'options particulières de compilation. Par exemple le fichier d'include de Scilab "machine.h" sera trouvé. Si l'utilisateur a besoin d'une option de compilation particulière (par exemple pour débuger) il peut aller la rajouter dans le fichier config/Makefile.all (ou comme on l'a déjà vu dans le fichier généré config/Makefile.incl).
SHELL = /bin/sh

include config/Makefile.incl

CFLAGS = $(CC_OPTIONS) -I$(SCIDIR)/routines -DSCI=\"$(SCIDIR)\"
FFLAGS = $(FC_OPTIONS) 
CPPFLAGS= $(CC_OPTIONS) -I$(SCIDIR)/routines -DSCI=\"$(SCIDIR)\"

all:: $(PROG) 

world:: $(PROG) 

include config/Makefile.Top
include config/Makefile 

distclean::
        $(RM) $(PROG)
        $(RM) -f -r  config ./configs 

5   Soyons plus moderne

Peut-être que certains lecteurs, à la lecture du mot Fortran, ont eu un sourire narquois <<c'est quoi cette revue moderne qui parle de langages préhistoriques>>. Pour leur redonner confiance dans leur revue préférée on va terminer sur une note plus joyeuse mais aussi plus difficile à écrire : on va utiliser JNI pour appeler Scilab à partir de classes Java. Commençons par la partie Java. Dans le code qui suit on écrit une classe Java nommée JavaSciMatrix dont les objets sont des matrices implémentées au moyen de tableaux de double unidimensionnels ( ceci pour avoir une implémentation voisine de celle de Scilab). L'interaction avec Scilab se fait au moyen de la méthode scilabJob qui a un unique argument à savoir une chaîne de caractères Java (un objet de la classe String). La méthode scilabJob doit <<envoyer>> la matrice de type JavaSciMatrix à Scilab, faire exécuter le job Scilab décrit par la chaîne scilabJob et retransferer les données en sens inverse de Scilab vers l'instance de JavaSciMatrix qui a appelée la méthode scilabJob. Pour que l'appel à la méthode scilabJob ait une chance de changer les données de l'objet qui l'invoque il faut bien sûr que le <<job>> Scilab contienne des références au nom de la matrice. Le lien entre la matrice JavaSciMatrix et sa correspondance en Scilab se faisant au travers de leur nom (champ name). Par exemple si on veut remplir une matrice avec des valeurs aléatoires calculées par Scilab on pourra utiliser :
 
    JavaSciMatrix p = new JavaSciMatrix("A",4,4);
    p.scilabJob(p.name + "= rand(" + p.m +"," + p.n +");");
Ainsi Scilab exécutera l'instruction "A=rand(4,4)"; Comme la construction de la chaîne est un peu laborieuse on pourra construire des méthodes enveloppantes pour rendre les choses plus lisibles. C'est ce que nous faisons dans le code qui suit pour les méthodes rand, show et inv. Par exemple la méthode rand  :
 
  public void rand() { 
    scilabJob(  name + "=rand(" + m +"," + n +");");
  }
permet de simplifier l'appel précédent en p.rand();. On notera que dans le code proposé, l'utilisateur doit respecter dans scilabJob les dimensions des matrices qui sont transmises. Nous ne vérifions pas cette cohérence. D'autre part notre code pourrait être optimisé : ici les transferts entre Java et Scilab sont effectués dans les deux sens et à chaque invocation de ScilabJob ce qui n'est pas forcément optimal. Voilà maintenant l'exemple complet coté Java :
 
class  JavaSciMatrix {
  private double [] x ; // Tableau pour les données 
  private int m,n;      // taille de la matrice mxn 
  String name;          // nom donné a la matrice 

  // constructeurs 
  JavaSciMatrix(String name,int m,int n) 
  {
    x = new double[m*n];
    this.m = m ;  this.n = n;
    this.name = name;
  }

  JavaSciMatrix(String name,int m,int n,double []x ) 
  {
    this.x = x ;  this.m = m ;  this.n = n;
    this.name = name;
  }

  // méthode <<native>> que nous implémenterons en C 
  public native void scilabJob(String job) ;

  // méthodes enveloppantes pour se faciliter la vie 
  public void inv() { scilabJob( name + "=inv(" + name +");");}
  public void show() {
    System.out.println("Matrix "+ name +"=");
    scilabJob( "disp(" + name +");");
  }

  public void rand() { 
    scilabJob(  name + "=rand(" + m +"," + n +");");
  }

  // A l'initialisation de la classe il faut charger le code externe 
  static 
  {
    System.loadLibrary("javasci");
  }

  // Un test de tout cela 

  public static void main(String[] args) {
    JavaSciMatrix m = new JavaSciMatrix("A",4,4);
    m.rand();  // on remplit m de nombres aléatoires (loi uniforme)
    m.show();  // un display fait par Scilab 
    JavaSciMatrix n = new JavaSciMatrix("B",2,2, new double [] {1,2,3,4} );
    n.show();  
    n.inv();   // Une inversion de matrice faite par Scilab 
    n.show();
  }
}
Le plus dur reste à faire. Implémenter la méthode <<native>> scilabJob. Comme le but de l'article n'est pas d'apprendre JNI on ne rentrera pas dans les détails. Un appel à javac pour compiler le code précédent, suivi d'un appel à javah permet d'obtenir un fichier d'entête JavaSciMatrix.h. On connaît ainsi le nom de la fonction C à implémenter. On écrit alors JavaSciMatrix.c :
#include "JavaSciMatrix.h"

#include <math.h>
#include <stdio.h> 
#include "machine.h"
#include "stack-c.h"

static int send_scilab_job(char *job) ;
static void Initialize() ;
void C2F(banier)(int *x) ;

JNIEXPORT void JNICALL Java_JavaSciMatrix_scilabJob
  (JNIEnv *env , jobject obj_this, jstring job)
{
  static int init = 0;
  int i,cm,cn,lp;
  const char *cname,*cjob;
  /* get the class */
  jclass class_Mine = (*env)->GetObjectClass(env, obj_this);
  /* get the fields i.e x,m,n,name  */
  jfieldID id_x = (*env)->GetFieldID(env, class_Mine, "x", "[D");
  jfieldID id_name =  (*env)->GetFieldID(env, class_Mine, "name", 
                                         "Ljava/lang/String;");
  jfieldID id_m = (*env)->GetFieldID(env, class_Mine, "m", "I");
  jfieldID id_n = (*env)->GetFieldID(env, class_Mine, "n", "I");
  /* get the field value */
  jdoubleArray jx = (*env)->GetObjectField(env, obj_this, id_x);
  jstring jname = (jstring) (*env)->GetObjectField(env, obj_this, id_name);

  jint jm = (*env)->GetIntField(env, obj_this, id_m);
  jint jn = (*env)->GetIntField(env, obj_this, id_n);
  double *cx = (*env)->GetDoubleArrayElements(env,jx,NULL);
  cname = (*env)->GetStringUTFChars(env, jname, NULL);
  cjob = (*env)->GetStringUTFChars(env, job, NULL);

  if ( init == 0) { init++; Initialize();} 
  cm=jm;cn=jn;
  if (!  C2F(cwritemat)((char *)cname,&cm,&cn,cx,strlen(cname))) 
    fprintf(stderr,"erreur lors de l'envoi a scilab \n");
  else { 
    if ( send_scilab_job((char *)cjob) != 0) 
      {
        fprintf(stderr,"Bug in scilab \n");
      }
    else 
      {
        if ( ! C2F(cmatptr)((char *)cname, &cm, &cn, &lp,strlen(cname))) 
          fprintf(stderr,"erreur lors de la reception \n");
        else 
          for (i=0 ; i < cm*cn ; i++) cx[i] = *stk(lp+i);
      }
  }
  (*env)->ReleaseStringUTFChars(env, jname , cname);
  (*env)->ReleaseStringUTFChars(env, job , cjob);
  (*env)->ReleaseDoubleArrayElements(env,jx,cx,0);
}

On reconnaît dans le code précédent la fonction send_scilab_job que l'on a déjà utilisée et on trouve aussi C2F(cwritemat) et C2F(cmatptr) qui ont les mêmes fonctions que les macros WriteMatrix et GetMatrixptr déjà vues. Les seules difficultés nouvelles sont donc de récupérer des pointeurs de chaîne C cname et cjob (nom de la matrice et job Scilab) des entiers jm et jn qui sont des champs de l'objet qui a invoqué la méthode et enfin un pointeur de double cx qui permettra d'accéder au vecteur des données. On accède aux champs d'un objet avec la fonction GetFieldID qui demande de connaître le nom du champ, la classe de l'objet (que l'on a obtenu avec GetObjectClass) et la signature du champ qui est une chaîne qui code le type du champ. La signature peut s'obtenir avec la commande javap -s JavaSciMatrix et on obtient par exemple la signature d'un tableau de double "[D". Une fois obtenu les champs de l'objet et connaissant les arguments (ici jstring job) il faut convertir ces arguments dans un format compris par C. Pour les chaînes de caractères la conversion est faîte au moyen de GetStringUTFChars :
 
 cname = (*env)->GetStringUTFChars(env, jname, NULL);
et il ne faut pas oublier de <<libérer>> la zone utilisée au moyen de ReleaseStringUTFChars. Pour le tableau de double GetDoubleArrayElements et ReleaseDoubleArrayElements jouent un rôle équivalent. Noter qu'en cas d'erreur Scilab on imprime juste un message d'erreur. Pour être plus propre, il faudrait renvoyer une Exception Java. JNI rend cela possible et nous renvoyons le lecteur à la doc
   http://java.sun.com/j2se/1.3/docs/guide/jni/index.html
Le code complet de JavaSciMatrix.c que l'on trouvera sur le CD contient aussi le code des fonctions déjà vues Initialize send_scilab_job et C2F(banier) et le code de la fonction MAIN__ que l'on réduit a sa plus simple expression.
 
int MAIN__() {};
Il nous faut maintenant compiler le code C. Cette fois, on ne veut pas créer une application mais plutôt une librairie partagée qui sera ensuite chargée par Java. On reprend le fichier Makefile précédent et cette fois on écrit :
 
SCIDIR=/usr/local/lib/scilab-2.5.1
PROG= libjavasci.so 
OBJS= JavaSciMatrix.o

#--------------------------------------------------------
# do not modify below 
#--------------------------------------------------------

all     :: .init 
        make -f config/Makefile.all FC="g77 -Wl,-shared" \
        PROG=$(PROG) OBJS="$(OBJS)" SCIDIR=$(SCIDIR)

..........

On a écrit dans PROG le nom de la librairie partagée. Noter que c'est le nom que l'on avait écrit dans System.loadLibrary("javasci"); précédé de lib et suffixé par .so. On rajoute également FC="g77 -Wl,-shared" (oui, c'est dans la partie do not modify below!) pour que le linker fasse une librairie partagée et pas un exécutable. Et maintenant c'est facile, on tape :
 
        make distclean 
        javac JavaSciMatrix.java 
        make all 

        setenv LD_LIBRARY_PATH `pwd`
        java JavaciMatrix 
et on obtient :
 
ekdahl% java JavaSciMatrix
Et Hop ....
 
Startup execution:
  loading initial environment
Fin de Init
Matrix A=
 
!   0.2113249    0.6653811    0.8782165    0.7263507 !
!   0.7560439    0.6283918    0.0683740    0.1985144 !
!   0.0002211    0.8497452    0.5608486    0.5442573 !
!   0.3303271    0.6857310    0.6623569    0.2320748 !
Matrix B=
 
!   1.    3. !
!   2.    4. !
Matrix B=
 
! - 2.    1.5 !
!   1.  - 0.5 !
Comme on est d'un naturel méfiant on vérifie le résultat :
 
-->B=[1,3;2,4] ; Binv=[-2,1.5;1,-0.5];
-->norm(B*Binv-eye(B)) 
 ans  =
 
    0.  
Et on conclut: pas si mal finalement l'inversion de matrices en Fortran !
This document was translated from LATEX by HEVEA.