Node.js Addons – Qu'est-ce qu'un addon ? – Dans node-libspotify



Node.js Addons – Qu'est-ce qu'un addon ? – Dans node-libspotify

0 1


nodejsparis-addons

Les addons natifs binaires en node.js avec libspotify

On Github Floby / nodejsparis-addons

Node.js Addons

avec node-libspotify

Hello world, notes

Qui suis-je ?

Florent Jaby

  • Consultant pour OCTO Technology
  • Nodeur depuis 2009
  • Auteur de node-libspotify

Qu'est-ce qu'un addon ?

  • Un module node.js écrit en C/C++ au lieu de JS
  • Un module node.js compilé
  • Peut invoquer directement des fonctions natives d'autres librairies ou du système

Utilisation de drivers, librairies tierces implementation d'un algorithme plus rapide en C

Concrètement, ça marche comment ?

  • Une librairie dynamique en .node au lieu de .so ou .dll
  • Node.js charge la librairie avec dlopen() lorsque l'on appelle require('module.node')
  • Node.js trouve le symbole du même nom avec dlsym() et éxecute la fonction d'initialisation

Une lib dynamique est du code machine avec une table de symboles. Node parcours cette table.

Fonction d'initialisation

void init (Handle <Object> exports) {
  exports->Set(String::NewSymbol("hello"), String::New("world"))
}

NODE_MODULE(hello, init)
                        

Utilisation

var addon = require('hello.node');
console.log(addon.hello);
// "world"
                      

Dans node-libspotify

extern "C" {
  void init (v8::Handle<v8::Object> target)
  {
    v8::HandleScope scope;

    // initializing all modules
    nsp::init_album(target);
    nsp::init_artist(target);
    nsp::init_link(target);
    nsp::init_player(target);
    nsp::init_search(target);
    nsp::init_session(target);
    nsp::init_track(target);
    nsp::init_playlistcontainer(target);
    nsp::init_playlist(target);

  }
}


NODE_MODULE(spotify, init);
            
Découpage de l'addon en plusieurs modules. Miroir avec les modules de libspotify noter le namespace NSP
void nsp::init_album(Handle<Object> target) {
  NODE_SET_METHOD(target, "album_is_loaded", Album_Is_Loaded);
  NODE_SET_METHOD(target, "album_name", Album_Name);
  NODE_SET_METHOD(target, "album_year", Album_Year);
  NODE_SET_METHOD(target, "album_type", Album_Type);
  NODE_SET_METHOD(target, "album_artist", Album_Artist);
  NODE_SET_METHOD(target, "album_cover", Album_Cover);
}

            
NODE_SET_METHOD est une macro C pour construire une fonction JS à partir d'une fonction C et l'attribuer à une objet. nommage des méthodes par libspotify "Voyons en détail ce que fait 'album_name'"
static Handle<Value> Album_Name(const Arguments& args) {
  HandleScope scope;

  // test arguments sanity
  assert(args.Length() == 1);
  assert(args[0]->IsObject());

  // gets sp_album pointer from given object
  ObjectHandle<sp_album>* album = ObjectHandle<sp_album>::Unwrap(args[0]);

  const char* name = sp_album_name(album->pointer);

  return scope.Close(String::New(name));
}


            
Expliquer ligne par ligne le fonctionnement de la fonction Expliquer ce qu'est Arguments Gestion de mémoire avec scode.Close Attirer l'attention du ObjectHandle

ObjectHandle<type> ?

  • Stocke un pointeur dans un objet JS
  • Similaire à ObjectWrap inclu dans node.h
  • Plus adapté à mon cas d'utilisation
Plus adapté parce que libspotify ne fournit que des pointeurs et que ObjectWrap est plus centré autour d'objets et classes.

Utilisation depuis JavaScript

var b = require('bindings')('spotify.node');

function Album (sp_album) {
    this._sp_object = sp_album;
    SpObject.apply(this);
}
util.inherits(Album, SpObject);

Album.prototype._populateAttributes = function _populateAttributes() {
    this.name = b.album_name(this._sp_object);
    this.year = b.album_year(this._sp_object);
    this.type = b.album_type(this._sp_object);
    this.artist = new sp.Artist(b.album_artist(this._sp_object));
};
// ...

Album.getFromUrl(spotifyUrl).whenReady(function () {
  this.name;
  this.year;
  this.type;
});

            

Multithreading

Un seul thread n'a accès à JavaScript Il faut parfoit temporiser des données

Un des nombreux écueils lorsqu'on dévéloppe des addons natifs

music_delivery

Je vous montrerai le code à la fin si vous êtes sages

Compilation

binding.gyp

{
  "targets": [
    {
      "target_name": "libspotify",
      "sources": [
        "src/mon_addon.cc",
      ]
    }
  ]
}
            
  • Le nom de l'addon compilé
  • La liste des fichiers sources
  • La liste des dépendances, s'il y en a
  • Plein d'autres options...
{
  "targets": [
    {
      "target_name": "libspotify",
      "sources": [
        "src/album.cc",
        "src/artist.cc",
        "src/audio.cc",
        "src/binding.cc",
        "src/link.cc",
        "src/player.cc",
        "src/search.cc",
        "src/session.cc",
        "src/track.cc",
        "src/playlist.cc"
      ],
      "cflags": ["-Wall"],
      "conditions" : [
        [
          'OS!="win"', {
            "libraries" : [
              '-lspotify'
            ],
          }
        ]
      ]
    }
  ]
}
            

Tests

npm test Attention, surprise ! c'est simple !
test-000-config.js
✔ testInstanciatingSessionWithNoArgumentThrows
✔ testInstanciatingSessionWithKey

test-010-session01-instance.js
✔ testInstanciatingSessionWithAppKeyPath
✔ testOnlyOneSession
✔ testStopSession

test-010-session02-login.js
✔ testLoginDoesntThrow
trying to login, this may take a while
✔ testLoginIsSucessful

test-020-search-01-create.js
✔ search - testSearchFromString
✔ search - testSearchGuillemotsSongFound

test-020-search-02-process.js
✔ testGetTrackFromSearchResult

test-030-track.js
✔ track - attributes are mapped

test-050-player00-track.js
playing track, this may take some time
✔ testPlaySingleAlJArreauTrack

test-031-album.js
✔ album - attributes are mapped
✔ album - cover image - default size
✔ album - cover image - normal size (function)
✔ album - cover image - small size (string)
✔ album - cover image - large size (constant)
✔ album - cover image - unknown size
(...)
test-080-playlist.js
✔ playlist - get playlist from URI
✔ playlist - attributes are mapped
✔ playlist - get tracks

test-999-cleanup.js
✔ Not actually a test, just cleaning up

OK: 115 assertions (24709ms)
================== Coverage summary ==================
Statements   : 84.43% ( 396/469 )
Branches     : 74.55% ( 82/110 )
Functions    : 72.63% ( 69/95 )
Lines        : 85.71% ( 384/448 )
======================================================
              

Pour finir

Conseils : KISS

  • Mapping le plus minimal possible JS <-> C/C++
  • TDD pour maîtriser la complexité
  • Tester l'interface JS
  • Utiliser les outils C/C++ que Node.js fournit
QUE DE LA GLUE Les Baby steps de TDD forcent à avoir le minimum de glue possibleO Node.js fournit des macro est des classes pour aider au développement d'addons

Difficultés courantes

  • Le langage
  • La compilation multi-plateforme
  • Les segfaults !
  • Le mutlithreading

Ce qui vient

  • Modules précompilés
  • Abstraction en C plutôt que C++
  • Une réécriture complète de la totalité des modules natifs !
mentionner module-foundry de nodejitsu You're gonna rewrite it anyway. Ils ne veulent plus subir les changements d'API de v8

Démo / Questions

Merci beaucoup !