mardi 7 octobre 2008
Suite des appels bloquants huilés en extension C/Ruby
Par lucas, mardi 7 octobre 2008 à 22:06 :: Scientifique
Disclaimer: ce post contient un hippopotame claudiquant et ne dois pas être pris comme une bonne manière d'écrire son code ou penser une architecture.
Bon, je n'ai pas fait de code propre à juste compiler et démarrer pour la preuve du hack, mais voici comment il faut procéder. A noter qu'il faut être un peu au fait des extensions C pour Ruby, je vous laisse chercher sur la toile, il y a peu d'articles mais on les trouve facilement.
Tout d'abord on crée une socketpair avec Ruby (on peut le faire en script).
int skt1, skt2;
VALUE sp, s1, s2;
ID id_socketpair = rb_intern("socketpair");
ID id_SOCK_STREAM = rb_intern("SOCK_STREAM");
ID id_Socket = rb_intern("Socket");
ID id_UNIXSocket = rb_intern("UNIXSocket");
ID id_ivar_socketpair = rb_intern("@@socketpair");
ID id_fileno = rb_intern("fileno");
sp = rb_funcall( rb_const_get( rb_mKernel , id_UNIXSocket ),
id_socketpair,
2,
rb_const_get( rb_const_get(rb_mKernel, id_Socket ) , id_SOCK_STREAM ),
INT2FIX(0));
s1 = rb_ary_entry( sp, 0 );
s2 = rb_ary_entry( sp, 1 );
s1 = rb_ary_entry( sp, 0 );
s2 = rb_ary_entry( sp, 1 );
/* get the fileno */
skt1 = FIX2INT(rb_funcall( s1, id_fileno, 0 ));
skt2 = FIX2INT(rb_funcall( s2, id_fileno, 0 ));
A partir de là, on peut lire et écrire dans les sockets skt1 et skt2, l'une est à réserver au thread de la librairie C qui bloque, l'autre est là pour le thread principal contenant ruby lui-même. Les lectures / écritures doivent être agrémentées d'un protocole perso, compliqué au possible (puisque vous devez faire attention aux sockets qui craquent, que la quantité de donnée lue est la bonne, la façon de la représenter, les signaux qui interrompent la lecture etc.). Elles servent également de synchronisation si besoin.
Du côté de Ruby, on peut soit tout faire en script avec un appel a la méthode kernel "select". En version C ça donne une boucle avec un truc comme
ret = rb_funcall( rb_mKernel, id_select, 4, fds, Qnil, Qnil, INT2FIX(1) );
Avec ret une VALUE qui sera Qnil s'il n'y a rien à lire. Le id_select c'est rb_intern("select"), le fds est un array contenant une des valeurs s1 ou s2 lors de la création de la paire de sockets. Le INT2FIX(1) à la fin, c'est le timeout en secondes.
A bien noter qu'un thread doit écrire et lire dans le même socket, vu qu'on lit d'un côté et écrit de l'autre, parfois on se mélange les pinceaux et on y comprend plus rien. Pour résumer, en C les "seules" choses qu'on a vraiment à faire c'est: avoir un thread qui fasse les appels bloquants si nécessaire (risque de devoir faire de la synchronisation supplémentaire) et signale à travers un protocole. Les parties "socketpair" et "select" écrites en C peuvent être faites en script, seule la partie qui ressort un "int C" après un appel à "fileno" est nécessairement à l'interface des deux langages.
Ouf, on y est, je crois que les gens qui auront suivit jusqu'ici sauront deviner les petits pièges, et le hack très moche qu'un tel workaround représente, mais bon, tant qu'on aura pas de threads systèmes, il va falloir faire avec les d'hippopotames claudiquants :) . A réserver pour les petites quantités de données à copier et les trucs "simples". Autant dire pas souvent ^^.
A noter que j'ai surtout fait se post car j'étais déterminé à trouver un workaround au problème, plus qu'à proposer un truc sérieux. Là, il faudrait que je me mette à lire tout les mécanismes de l'intérieur du Ruby officiel de Matz.