Estendere le classi builtin di IronRuby con nuovi metodi
In precedenza abbiamo visto come con IronRuby sia già possibile estendere a runtime le classi builtin sfruttando lo stesso Ruby e il suo concetto di classi aperte, tuttavia molti metodi che noteremo mancare in queste prime fasi di sviluppo, come MutableString#reverse usato per il nostro esempio ma anche molti altri , fanno parte del set di metodi "standard" delle classi builtin dell'interprete Ruby originale di Matz. L'ideale sarebbe quindi implementarli direttamente in IronRuby, guadagnandone anche in prestazioni, ma la sorpresa è che questo lavoro non è per nulla complicato.
Muoversi all'interno dell'archivio con i sorgenti è semplice, nella directory Docs è presente un readme.htm (tra l'altro realizzato con TiddlyWiki) con alcune indicazioni sulle linee guida per la scrittura del codice (SourceCodeLayout) e un paragrafo che fa al caso nostro: come aggiungere un metodo a una classe builtin (AddAMethod). In Src abbiamo la directory eMicrosoft.Scripting e Ruby, rispettivamente i sorgenti del DLR e di IronRuby. I file su cui andremo a lavorare sono contenuti in Src\Ruby\Builtins dove si trovano le implementazioni di tutte le classi builtin di IronRuby.
A questo punto apriamo il file Src\Ruby\Builtins\MutableString.cs, scriviamo il nostro metodo all'interno della classe MutableString e vediamo di commentarlo:
1 [RubyMethodAttribute("reverse", RubyMethodAttributes.PublicInstance)] 2 public static MutableString Reverse(MutableString str) { 3 if (str.Length <= 1) 4 return new MutableString(str); 5 6 char[] reversed = new char[str.Length]; 7 int len = str.Length - 1; 8 for (int i = 0; i <= len; i++) 9 reversed[i] = str[len - i]; 10 11 return new MutableString(new String(reversed)); 12 }
- riga 1: per specificare il nome che verrà usato in Ruby per il metodo e la sua visibilità sono stati creati rispettivamente l'attributo RubyMethodAttribute e l'enum RubyMethodAttributes. Come spiegato nel documento allegato, il motivo per cui viene utilizzato RubyMethodAttribute è semplicemente dovuto al fatto che in Ruby possono esistere nomi di metodi non validi in C# come empy? oppure downcase!, inoltre in questo modo è possibile ricreare la moltitudine di operatori come <=> (comparazione) oppure =~ (corrispondenza).
- riga 2: i metodi che andremo a creare sono statici poiché in realtà il DLR sfrutta una propria implementazione degli extension method di C# 3.0, in cui i metodi di estensione si presentano appunto come statici e il primo argomento è rappresentato da this per i metodi di istanza. In questo caso, il primo argomento sarà rappresentato dall'oggetto chiamante (è come se da Ruby arrivasse self).
Commentiamo rapidamente l'implementazione, saltando solo la parte che si occupa semplicemente di invertire la stringa:
- riga 3-4: se la stringa è vuota oppure è formata da un solo carattere allora è inutile proseguire dal momento che non c'è molto da invertire, tuttavia ritorniamo ugualmente una nuova istanza di MutableString poiché questo è il comportamento di Ruby, basta osservare in IRB con l'interprete originale come ogni chiamata a String#reverse restituisca un oggetto con un object_id sempre differente:
irb(main):001:0> "CIAO".reverse.object_id => 23652180 irb(main):002:0> "CIAO".reverse.object_id => 23648140 irb(main):003:0> "C".reverse.object_id => 23644340 irb(main):004:0> "C".reverse.object_id => 23640540 irb(main):005:0> "".reverse.object_id => 23636820 irb(main):006:0> "".reverse.object_id => 23633100
- riga 11: non ci sono costruttori di MutableString che prevedano un array di caratteri come parametro, per cui creiamo una istanza intermedia di String partendo dal nostro array di caratteri e creiamo da quella la nostra nuova istanza di MutableString. Avremmo potuto fare in altro modo ma facciamoci andar bene questa riga, soffermarsi a questo punto dei lavori sui dettagli implementativi per spaccare il byte è abbastanza inutile visto che molte parti saranno soggette a cambiamenti nel corso dello sviluppo di IronRuby e del DLR.
Per compilare il tutto correttamente in base alle nostre modifiche, seguiamo alcuni passi:
- Modificare il file Build.cmd cambiando la configurazione da modalità Release a Debug ( /p:Configuration=Debug ) e poi modificare il file Src\Ruby\Builtins\GenerateInitializers.cmd togliendo il ..\ iniziale (c'è un errore nei path).
- Eseguire Build.cmd. Questa prima compilazione genererà l'utility Bin\Debug\ClassInitGenerator.exe che lanceremo attraverso Src\Ruby\Builtins\GenerateInitializers.cmd e che (ri)genera automaticamente il file Src\Ruby\Builtins\Initializer.Generated.cs contenente i vari dispatcher ai metodi. Ogni volta che a una classe builtin aggiungiamo un metodo visibile da Ruby, dobbiamo rigenerare questo file (si può anche editare a mano, ma è ovviamente meno comodo e aumenta la possibilità di errore).
- Lanciare nuovamente Build.cmd per compilare il progetto con il nostro Initializer.Generated.cs aggiornato per riflettere l'aggiunta del metodo MutableString#reverse.
Non ci resta da fare altro che verificare i risultati tramite RBX, lanciandolo da Bin\Debug\rbx.exe:
>>> "CIAO".reverse => "OAIC" >>> "CIAO".reverse.object_id => 43 >>> "CIAO".reverse.object_id => 44 >>> "C".reverse.object_id => 45 >>> "C".reverse.object_id => 46 >>> "".reverse.object_id => 47 >>> "".reverse.object_id => 48
La stringa viene correttamente invertita e il comportamento è il medesimo di quello riscontrato in IRB con l'interprete Ruby originale.