mercoledì 12 settembre 2012

Un Preprocessore per Java ;-)

Premesse

Ci sono un paio di migliaia di cose che non apprezzo del linguaggio Java, l'ho sempre considerato una specie di : "C++ for dummies", ma sono solo gusti personali e possono essere trascurati.

Ciò che proprio non mi riesce di trascurare invece è l'assenza di un tranquillo, comodo, semplice preprocessore.

Eh lo sò. Anche io ho lavorato in varie aziende e anche a me è passato per le mani qualche documento, sulle policy di sviluppo del software con scritte le solite cose e quasi sempre in bella mostra : "Il preprocessore è da evitare in quanto rende il codice illeggibile".

Da qui l'ovvia e conseguente domanda e cioè ... allora perché l'hanno messo ? O forse negli anni 70 erano tutti dei truzzi strafumati che se ne fregavano di come era scritto il software ? ... Vi prego non rispondete !

Tornando a Java poi, il discorso della leggibilità è molto più acuto, specie quando ci si trova di fronte ai makefiles di ant, a inner classes con relativa istanza final static, a vagonate di singleton e a simpatici workaround, tipo che per passare un intero per riferimento devi allocare dinamicamente un array.

Tralasciamo quindi il ragionamento anche lui "for dummies", e diamo all'ingegnere ciò che è dell'ingegnere, cioè la possibilità di usare almeno in piccola parte il cervello.

Ebbene : sarà stata forse proprio la fiducia smodata in questa massa grigia e semiseria che ha convinto gli sviluppatori di NetBeans, ad introdurre anni fa uno pseudo preprocessore in grado di escludere interi pezzi di codice quando non utilizzati.

Ricordo di averlo usato, per lo sviluppo di applicazioni in J2ME e di averlo trovato molto utile, per evitare centinaia di copia incolla ed immensi lavori di maquillage.

Va anche detto che NetBeans, ha delle proprietà interessanti che non ho trovato (nativamente) nella piattaforma più usata per lo sviluppo Java e cioè Eclipse, tipo la possibilità di escludere blocchi di codice dall'editazione, i quali permettono un sacco di trucchetti.

Pazienza, faremo a meno degli aiuti e ci addentreremo nel pericoloso mondo della modificabilità dell'immodificabile, lasciando perdere ulteriori considerazioni su linguaggi e IDE ideali e tornando al dunque ...

Dunque

Volendo sviluppare per piattaforme il cui ambiente di lavoro è basato su Eclipse, nella fatispecie Android, ho avuto per esempio la necessità di simulare il comportamento di un sensore non presente sull'emulatore e quindi, realizzare una classe con una parte da fare entrare in funzione solo quando ero su un device virtuale e non quando facevo il deploy.

Stessa cosa dicasi per i log da scrivere solo in fase di DEBUG.

Per evitare di creare nuove (e illeggibili) infrastrutture di programma o turbare l'instabile equilibrio del sistema di sviluppo di Android con remoti plugins, ho cercato una soluzione un po` più semplice e "aperta", qualcosa in stile NetBeans che mi permettesse semplicemente di commentare in modo automatico, con commenti riconoscibili, interi blocchi di testo java per poi rimuoverli a comando.

Così ed è nato il JavaProc, ed è un .py ovviamente.

JavaProc

Il JavaProc è un piccolo programma in Python che si può chiamare dall'IDE come processore esterno.

Eccolo qui :



#!/usr/bin/python
# JavaProc
# A smart preprocessor for Java Language - Eclipse Platform
# (c) 2012 by El Duraminga
# Please visit my site at http://elduraminga.blogspot.it/

# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESSED OR IMPLIED
# WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE DEVELOPERS OR CONTRIBUTORS BE LIABLE FOR ANY
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
# GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING
# IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#

import sys
import os
import shutil
import tempfile


if len(sys.argv)<2 :
  print "No path specified"
  exit
pth=sys.argv[1]
area={}


def defined(name) :
  global area
  if area.has_key(name) :
    return area[name]
  return False



def analyze(filename) :
  global area
  modified=False
  vect=[]
  fp=tempfile.NamedTemporaryFile(delete=False)
  fs=open(filename,"r")
  dest=fp.name 
  for ln in fs:
    lx=ln.split()
    if len(lx)==2 and lx[0]=='//#define' :
      area[lx[1]]=True
      fp.write(ln)
    elif len(lx)==2 and lx[0]=='//#undefine' :
      area[lx[1]]=False
      fp.write(ln)
    elif len(lx)==2 and lx[0]=='//#ifdef' :
      vect.append([lx[1],True])
      fp.write(ln)
    elif len(lx)==2 and lx[0]=='//#ifndef' :
      vect.append([lx[1],False])
      fp.write(ln)
    elif len(lx)==1 and lx[0]=='//#endif' :
      vect.pop();
      fp.write(ln)
    elif len(lx)==1 and lx[0]=='//#else' :
      a=vect.pop();
      if a[1] :
        vect.append([a[0],False])
      else :
        vect.append([a[0],True])
      fp.write(ln)
    else :
      h=True
      for i in vect :
        if i[1] :
          if not defined(i[0]) :
            h=False
        else :
          if defined(i[0]) :
            h=False
      if h :
        if len(ln)>2 and ln[0:3]=="//$" :
          modified=True
          fp.write(ln[3:])
        else :
          fp.write(ln)
      else :
        if len(ln)>2 and ln[0:3]=="//$" :
          fp.write(ln)
        else :
          modified=True
          fp.write("//$"+ln)
  fs.close()
  fp.close()
  if modified :
    shutil.move(dest,filename)
    return "Modified"
  os.remove(dest)
  return "Untouched" 


# Search for the config file
pthcfg=os.path.join(pth,"javaproc.cfg")

if not os.path.exists(pthcfg) :
  print "duraproc.cfg not found"
  exit

fs=open(pthcfg,"r")

for ln in fs:
  code=1;
  ln=ln.strip()
  if len(ln)>1 :
    if ln[0]=='-' :
      area[ln[1:]]=False
    elif ln[0]=='+' :
      area[ln[1:]]=True
    else : 
      area[ln]=True
               
fs.close()


# Change the required files

for fpth, dirnames, fnames in os.walk(pth):
  for fname in fnames:
    f=os.path.join(fpth,fname)
    if (len(f)<5) :
      continue
    if f[len(f)-5:]!=".java" :
      continue
    x=analyze(f)
    print "Processed : "+f+" - "+x




Chi conosce la grandiosità di Python, non si stupirà per le poche righe di programma e nemmeno del fatto che pur essendo stato scritto per Linux, dovrebbe andare bene anche in Windows, o su MAC.

Come funziona

Il primo file che va creato è javaproc.cfg.  Per una esplicita volontà del programmatore (cioè di me), questo file è obbligatorio anche se vuoto, così funge anche da "stop", per bloccare l'eventuale processing di cartelle nel quale il file non è presente.

Và messo nella root del progetto dove si vuole applicare il preprocessore e sostituisce il config.h che spesso si trova nei progetti C.

Molti infatti in C, al di là degli switch di make, mettono una serie di #define all'interno di un .h  con indicate le varie costanti del codice, poi il file è incluso ovunque.

In Java, ciò non è auspicabile, in quanto l'include non è un insert del codice ma si chiama import ed è un join dinamico, dunque javaproc.cfg  non è un normale file java ma un semplice elenco di parole con eventualmente un simbolo + o - davanti.

Per esempio javaproc.cfg potrebbe essere così :

+DEBUG
-SIMUL
MODIF


Che significa un #define di DEBUG e di MODIF e un #undefine di SIMUL.

Si evince naturalmente che tutto ciò che non è dichiarato in questo file è undefinedma mettere un - è sicuramente più comodo che cancellare tutte le volte una variabile e reinserirla a memoria.

Vediamo ora le modifiche nel codice ...

Si deve inserire all'inizio della riga del blocco da escludere o includere un semplice #ifdef o un #ifndef come se si usasse il preprocessore originale ma non sono ammesse operazioni aritmetiche né logiche, inoltre dobbiamo commentare, in questo modo :

//#ifdef DEBUG
       Log.i("MYAPP","Remember to log");
       Log.i("MYAPP","and then");
//#endif 

I due tag saranno visti come semplici commenti dal compilatore e quindi ignorati.

È possibile usare :

//#ifdef
//#ifndef
//#endif 
//#else

ma è anche possibile usare ...

//#define
//#undefine

in questo modo :

//#define NEWVAR
//#undefine OLDVAR

Sempre senza parametri, permettendo una gestione locale esclusivamente all'interno del file e dopo il punto in cui sono inserite esattamente come in C.

Lanciare il preprocessore

Bene, dopo avere preparato un esempio, dobbiamo permettere l'utilizzo del preprocessore da Eclipse.

A differenza di quanto si può pensare questo non è un problema, infatti sotto il menù :

Run->External Tools->External Tools Configurations ...

troviamo :


Come vediamo, dopo avere reso eseguibile il proprocessore con chmod 755 se su Linux o MAC o invocato dal relativo interprete in un .bat su Windows, possiamo inserire la posizione di ciò che vogliamo rendere eseguibile nella Location, mentre come argomenti mettiamo ${project_loc} che ci darà il path del progetto, il quale deve essere il root folder dove è contenuto direttamente il javaproc.cfg (e presumibilmente src con tutte le sottocartelle).

Dopo avere confermato, scegliamo il progetto dal Project Explorer e direttamente sulla cartella lanciamo negli External Tools, il nuovo preprocessore javaproc.

Provvederemo poi volendo, ad assegnargli un tasto rapido, in Eclipse esiste il comando per ripetizione dell'ultimo external tool, sul mio sistema l'ho piazzato su F7.

Un volta lanciato, il programma prima interpreterà il contenuto del file javaproc.cfg e poi passerà alle sottocartelle, tenendo in considerazione solo i files che hanno estensione ".java".

Nella Console, di Eclipse, vedremo così tutti i files esaminati, contrassegnati da "untouched" e quelli realmente modificati, contrassegnati da "modified".

Avendo +DEBUG nel file javaproc.cfg che abbiamo preso ad esempio e il nostro file con #ifdef DEBUG troveremo questa situazione :

//#ifdef DEBUG
//$       Log.i("MYAPP","Remember to log");
//$       Log.i("MYAPP","and then");
//#endif 

Cioè saranno stati aggiunti simboli //$ di fronte alle righe tra i blocchi secondo la logica del preprocessore, trasformandole in commenti.

Per chi si chiedesse cosa ci fa il simbolo del $ dopo //, dico che serve per distinguere ciò che è un possibile commento pregresso da ciò che è stato inserito dal preprocessore e che quindi è molto importante che rimanga dove è.

Se mettiamo un - di fronte a DEBUG il file sarà nuovamente ritoccato per tornare alla situazione precedente, cancellando appunto i tag //$ interessati.

Conclusioni

Questo preprocessore "asincrono" sembra funzionare correttamente, fino ad ora non ha dato problemi e mi ha permesso per esempio di realizzare due versioni dello stesso programma con un solo codice sorgente, gestire simulazioni, debug e funzioni opzionali senza eccessivo dispendio di codice.

Ovviamente non è assolutamente esente da bug e può causare seri danni al progetto su cui lavorate e/o cancellarvelo allegramente e completamente quindi USATELO A VOSTRO RISCHIO ... ma la vita va presa con avventura no ?

Magari dateci una occhiata e miglioratelo, però lasciate il link a questo sito, caso mai a qualche pazzo venisse in mente di leggere il codice e volesse fare un autarchico giro da queste parti.