Journal on joue avec python ffi et json

Posté par . Licence CC by-sa
Tags :
24
15
mai
2018

intro

En moulant sur lobsters - oui je suis très fruits de mer - je tombe sur un article du blog libtorrent. Il explique que le parser Bencode - le format des fichiers torrent - a été ré-écrit pour être plus rapide en s'inspirant de l'approche minimaliste du parseur JSON jsmn.

Ce parseur est qualifié de demi-parseur dans le billet de blog car il ne fait que repérer la structure du document JSON (clés, valeurs, hiérarchie) mais n'implémente pas d'interface compliquée comme DOM, pushparser ou pullparser.

Une de ses grosses limitations, liée au fait que l'auteur ne veut pas faire usage d'allocations dymaniques, est qu'il faut borner le nombre d'éléments de structure dans le document.

C power

J'ai téléchargé le code depuis son git. Le code est très simple à prendre en main. J'ai un peu joué avec pour faire un exemple tout simple qui montre comment l'outil fonctionne.

#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include "../jsmnlib.h"

int main() {
    jsmn_parser p; jsmn_init(&p);
    char *sample = "{\"user\": \"johndoe\", \"admin\": false, \"uid\": 1000,\n\"groups\": [\"users\", \"wheel\", \"audio\", \"video\"]}";
    jsmntok_t t[16]; /* We expect no more than this amount of tokens */
    int r = jsmn_parse(&p, sample, strlen(sample), t, 16);
    for (int i = 0; i < r; i++) {
        jsmntok_t tok = t[i];
        printf("%d, %d, %d, %d, %d\n", i, tok.type, tok.start, tok.end, tok.size);
    }
    return EXIT_SUCCESS;
}

Cela produit la liste des éléments du document avec le type, la position et le nombre d'enfants.

 0, 1,  0, 96, 4 <= objet sans nom, prenant tout le buffer
 1, 3,  2,  6, 1 <= clé user
 2, 3, 10, 17, 0 <= valeur string de user
 3, 3, 21, 26, 1 <= clé admin
 4, 4, 29, 34, 0 <= valeur primitive de amdin : false
 5, 3, 37, 40, 1 <= clé uid
 6, 4, 43, 47, 0 <= valeur primitive de uid : 1000
 7, 3, 50, 56, 1 <= clé "gourps"
 8, 2, 59, 95, 4 <= valeur array "groups", a 4 fils.
 9, 3, 61, 66, 0 <= 1. users
10, 3, 70, 75, 0 <= 2. wheel
11, 3, 79, 84, 0 <= 3. audio
12, 3, 88, 93, 0 <= 4. video

pythonification

Comme je suis plus à l'aise avec python qu'avec C, je me suis mis en tête de rendre jsmn disponible en python. Une approche pourrait être de l'écrire de zéro ; cela ne présenterai pas de grosse difficulté. Mais dans la perspective de faire un peu de perf, je me suis dis qu'interfacer le code natif pourrait être une bonne solution. Cette technique se nomme Foreign function interface (FFI) et il y a bien évidemment un module pour faire ça dans python.

apt-get install python3-cffi

préparation du code natif

Pour que le code natif soit pris en charge, il est nécessaire de:

  • produire une bibliothèque logicielle partagée (".so") au lieu de statique (".a")
  • épurer les déclarations d'entêtes (".h") de toutes les instructions pré-processeur que cffi ne gère pas.

Pour la première contrainte, j'ai complété le "Makefile"

libjsmn.so: jsmn.c jsmn.h
    `{mathjax} (CC) -shared -o `@ $<

Pour la seconde, j'ai supprimé toutes les directives pour ne garder que les structures de données comme "jsmntok_t" et les signatures de fonctions comme "jsmn_parse()".

côté python

cffi a une bonne documentation et il est très facile d'implémenter notre cas plutôt simple.

from cffi import FFI
ffi = FFI()

ffi.cdef(open("jsmnlib.h").read()) # charge l'API : structures de données et signatures de fonctions
jsmnlib = ffi.dlopen("./libjsmn.so") # charge la bibliothèque partagée

parser=ffi.new("jsmn_parser*")
jsmnlib.jsmn_init(parser)

sample = b'{"user": "johndoe", "admin": false, "uid": 1000,\n"groups": ["users", "wheel", "audio", "video"]}'

MAX_TOKENS = 16
t = ffi.new("jsmntok_t[]", MAX_TOKENS)
r = jsmnlib.jsmn_parse(parser, sample, len(sample), t, MAX_TOKENS)
for i in range(r):
    tok = t[i];
    print("%d, %d, %d, %d, %d" %( i, tok.type, tok.start, tok.end, tok.size))

Ce code python produit strictement le même résultat que le code C, inutile de le décrire à nouveau.

outro

L'idée de ces manipulations est de comparer les performances des divers implémentations :

  • jsmn_c versus jsmn_python
  • jsmn_python versus json_python

Pour cela il faut des cas de tests un peu réalistes et un jeu de données conséquent. Je ne me suis pas encore attelé à la tâche. Ce sera peut-être l'objet d'un autre journal.

Cette première étape m'aura permis de franchir le pas du FFI en python et je sais que cela me sera à nouveau utile un jour.

Envoyer un commentaire

Suivre le flux des commentaires

Note : les commentaires appartiennent à ceux qui les ont postés. Nous n'en sommes pas responsables.