mardi 22 décembre 2015

Utiliser ERB en c++, ou comment transformer un language sexy comme ruby en quelque-chose qui fait peur !

On a beau dire, le c et le c++ sont quand même les langages les plus puissant qui existent (surtout c++ en fait), mais il sont complexes et surtout ne sont pas interprétés !

Pour pallier certain manques de ces langages il est possible de rendre scriptable une application, via une api vers un langage interprété.

Ici on s’intéresse au problème suivant: on veut permettre à l'utilisateur d'inclure certaines commande pour rendre dynamique un document texte qu'il éditerai depuis une application écrite en c++.

J'ai cherché diverses solutions et c'est finalement Ruby qui a retenus mon attention, notamment à cause d'erb, le langage de template intégré à Ruby.

Prenons un exemple simple, un petit code ruby utilisant erb pour imprimer un petit hello world:


#!/usr/bin/ruby

require 'erb'

my_template = "Hello World\nToday is <%# this is just a comment %> <%= MyModule.MyFunc()%>\n"

module MyModule
          def self.MyFunc()
                    Time.now.strftime('%A')
          end
end

my_erb = ERB.new(my_template)
puts my_erb.result()


Mais maintenant, addmettons que l'on veuille faire la même chose en c++, cad au moins pouvoir créer une fonction qui puisse employer les données de nos variables c++ pour les employer dans ruby.

Pour ce faire on va passer par l'API c de ruby, pour ceux qui ne connaissent pas un petit lien sur le tuto !


Le premier défis consiste à obtenir les fichiers de développement ruby, sous ubuntu 15.04 il suffit d'installer ruby-all-dev:

sudo apt-get install ruby-all-dev

Si comme moi vous souhaitez aussi employer pkg-config il faut installer aussi ruby-pkg-config

sudo apt-get install ruby-pkg-config

Voilà, nous sommes prêts à utiliser ruby à l'intérieur de c ou c++. Donc, ça donne quoi le script d'en dessus en c++ ?


#include <iostream>
#include <ruby.h>

#include <cstdio>
#include <ctime>


using namespace
std;

const string erb_line = "Hello World\nToday is <%# this is just a comment %><%= MyModule.MyFunc()%>\n";

VALUE MyFunc(int argc, VALUE* argv, VALUE self) {
  
    time_t rawtime;
    tm* timeinfo;
    char buffer [80];

    time(&rawtime);
    timeinfo = localtime(&rawtime);

    strftime(buffer,80,"%A",timeinfo);
  
    string timeString(buffer);
  
    return rb_str_new2(timeString.c_str());
  
}

int main(int argc, char ** argv){
  
    /* construct the VM */
    ruby_init();
  
    /* Ruby goes here */
    ruby_init_loadpath();
    rb_require("erb");
  
    // Define a new module    
    VALUE mymodule = rb_define_module("MyModule");
    //add a function to the module
    rb_define_module_function(mymodule, "MyFunc", (VALUE (*)(...)) MyFunc, -1);

    //get a reference to ERB
    ID sym_erbclass = rb_intern("ERB");
    VALUE erbclass = rb_const_get(rb_cObject, sym_erbclass);
    //list of arguments to initialise erb (1 arg)
    VALUE erbargv[1];
    erbargv[0] = rb_str_new2(erb_line.c_str());
    VALUE myerb = rb_class_new_instance(1, erbargv, erbclass);
  
    //prepare the result
    VALUE resultargv[0];
    VALUE result = rb_funcallv(myerb, rb_intern("result"), 0, resultargv);
  
    cout << string(RSTRING_PTR(result), RSTRING_LEN(result));

    /* destruct the VM */
    return ruby_cleanup(0);
  
}

Un peu plus long n'est-ce pas ? Mais ce n'est pas tant sorcier que cela, c'est juste pénible à chercher dans la doc quand on est dans un cas de figure que les gens rencontrent apparement peu et donc ne documentent guère plus.

Il existe néanmoins des subtilité induite par c++, qui est par exemple plus fortement typé que c, ce qui explique qu'il faille caster de manière judicieuse le pointeur d'une fonction c++ que l'on veut enregistrer dans ruby.

La compilation ne pose pas de problème majeur, le plus dur et de localiser les headers dont dépend <ruby.h> pour les inclures dans le path (ci dessous la ligne que j'ai employé sur ubuntu 15.04):

g++ --std=c++11 main.cpp -o essai -I/usr/include/ruby-2.1.0 -I/usr/include/x86_64-linux-gnu/ruby-2.1.0/ -lruby-2.1

Vous me direz, tant d'efforts pour pas grand chose ? Je vous rapelle que le but était de pouvoir exploiter les données contenues dans une application c++, hors vous remarquerez que MyFunc peut tout à fait servir d'accesseur ! Ainsi nous disposons de toutes les cartes pour créer des applications exploitant erb.

La prochaine fois je vous montrerai comment cross-compiler une telle application pour Windows depuis Linux !