Eseguire codice nativo (DLL in C) in ambiente Java: Clear Screen, Info Hardware ed altro

« Older   Newer »
 
  Share  
.
  1.     +6    
     
    .
    Avatar

    Senior Member

    Group
    Manager
    Posts
    10,796
    Reputazione
    +266

    Status
    JNI: Eseguire codice nativo (linguaggio C) utilizzando Java
    Interamente a cura di Marco 'RootkitNeo'


    Prefazione

    Tra le domande più comuni quando si inizia a programmare in Java è sempre presente la seguente: è possibile usare dell'altro codice nei nostri programmi Java?
    La risposta a questa domanda è "Ni", o meglio... "JNI"! Iniziamo con il precisare una cosa scontata: non è possibile ovviamente mischiare codice Java e codice C/C++, ma è possibile eseguire del codice nativo (sia esso C, C++ o assembly) a patto che venga utilizzato Java Native Interface. L'interfaccia nativa (JNI) consente ad un software Java di caricare una DLL ed eseguirla. Questa pratica non è così diffusa, ma può tornare utile quando si necessitano prestazioni superiori.
    Purtroppo però, eseguendo codice nativo, ci si trova davanti ad un problema: la portabilità del codice. Quando si esegue una DLL ci si limita ad eseguire il software in ambiente Windows (ed ovviamente aumentano i rischi per la sicurezza trattandosi di codice nativo).

    Il presente articolo tratterà proprio questo argomento, con una particolare attenzione alla parte più pratica.

    Introduzione

    Prima di mostrare alcuni esempi è necessario dotarsi di alcuni strumenti. Primo tra tutti, il compilatore Java (JDK), scaricabile dal sito della Oracle. Io al momento mi trovo in ambiente Windows a 32bit, pertanto l'articolo si basa proprio su questo assunto. Il secondo strumento indispensabile è un compilatore per il linguaggio C, io sto utilizzando MinGW che potete scaricare al seguente indirizzo, ma se preferite utilizzarne altri ovviamente va bene (tuttavia i comandi per generare la DLL non saranno gli stessi).

    Ora che abbiamo tutto l'occorrente possiamo procedere. Prima di tutto: in che modo viene chiamata una DLL? Il procedimento purtroppo non è dei più semplici. Il primo passo necessario è dichiarare un metodo nativo, utilizzando proprio la parola chiave native. Uno dei metodi che troveremo nel corso dell'articolo è il seguente:

    CODICE
    public native void cls();


    Il funzionamento è sostanzialmente identico a quello dei comuni metodi, viene chiamato allo stesso modo dal programmatore.
    Non sapendo bene su quali esempi basare l'articolo, ho scelto l'esecuzione di codice di sistema, utilizzando la WINAPI. Gli esempi saranno 3, ciascuno dei quali utilizzerà diverse funzioni della WINAPI; un quarto esempio invece mostra i 3 software "uniti".
    Il primo che analizzeremo sarà JavaWindowsShell che permetterà di svolgere le seguenti funzioni:

    • Pulizia del prompt dei comandi;

    • Cambio del colore foreground e background (del testo) del prompt;

    • Posizionamento del cursore;


    Il secondo software sarà invece HardwareInformation che permetterà di svolgere le seguenti operazioni:

    • Ottenere il numero dei processori;

    • Ottenere il tipo di processore;

    • Ottenere l'architettura del processore;


    Le funzionalità sono poche, ma ampliarlo non sarà complicato (vi sono ancora altri campi della struttura SYSTEM_INFO che possono essere utilizzati, se lo volete).

    Il terzo software sarà MemoryInformation e permetterà di svolgere i seguenti compiti:

    • "Lunghezza" della memoria;

    • Carico della memoria;

    • Totale memoria fisica;

    • Memoria fisica disponibile;

    • Totale memoria virtuale;

    • Memoria virtuale disponibile;


    Il quarto ed ultimo software permetterà all'utente quali informazioni mostrare sull'hardware (utilizzando un semplice menu testuale), ed ingloberà la pulizia del prompt dei comandi (sfruttando il primo software, o per meglio dire, la prima libreria).

    Iniziamo quindi con il primo software!

    JavaWindowsShell: funzionalità per prompt dei comandi (clear shell)

    La prima cosa da fare, è creare un file Java contenente i metodi nativi che potranno poi essere chiamati durante l'esecuzione. Il file inoltre si occuperà del caricamento della DLL (che creeremo in seguito).
    JavaWindowsShell.java

    Codice

    Come vedete i metodi nativi non hanno un corpo, ma vengono solo dichiarati. Questo perché la loro definizione spetterà alla parte nativa, e avverrà quindi in C. Il blocco statico carica semplicemente la nostra DLL.

    Prima di procedere dobbiamo occuparci di un file chiamato jni.h, che è l'header file utilizzato poi da C. Questo file si trova nella directory del JDK, precisamente al seguente percorso (nel mio caso): "C:\Program Files\Java\jdk1.7.0\include". Copiamo il file jni.h ed incolliamolo nella directory degli include del nostro compilatore (MinGW) così da evitare il percorso completo ogni volta. La directory di MinGW nel mio caso è in: "C:\MinGW\include".
    Torniamo nella directory di Java e precisamente questa volta in "C:\Program Files\Java\jdk1.7.0\include\win32" e copiamo il file header chiamato jni_md.h; incolliamolo sempre nella directory dei file header di MinGW.

    A questo punto possiamo tornare al codice Java scritto in precedenza. Ciò che è necessario fare ora è compilare il codice appena scritto con il solito javac:
    CODICE
    javac JavaWindowsShell.java


    se vengono riportati errori controllate la sintassi (ed assicuratevi di settare Java e MinGW nella variabile d'ambiente PATH).
    Dobbiamo generare adesso un header file, che sarà utilizzato all'interno del nostro programma C, e che conterrà la dichiarazione delle funzioni che andremo a definire nel codice in C. Per generare l'header si utilizza il programma javah, sempre incluso nel JDK e nella stessa directory di javac e java.

    CODICE
    javah JavaWindowsShell


    Il file header che viene creato avrà questo aspetto:

    Codice

    Questo file merita assolutamente qualche parola. Prima di tutto, come scritto nel source, non deve essere modificato. Guardiamo in che modo si presentano le nostre funzioni native:

    CODICE
    JNIEXPORT void JNICALL Java_JavaWindowsShell_cls (JNIEnv *, jobject);


    Il metodo dichiarato nel file Java è cls(), ma qui compare anche il nome della classe. Ricordate sempre che un'altra classe potrebbe dichiarare lo stesso metodo; in questo modo ci si assicura di chiamare il metodo corretto.

    Ottenuto anche l'header file da javah, è ora necessario scrivere il corpo delle nostre funzioni. L'intestazione del file C che andremo a scrivere deve avere i seguenti include:
    CODICE
    #include "JavaWindowsShell.h"
    #include<windows.h>


    Il primo file che includiamo contiene la definizione delle nostre funzioni, il secondo è la libreria di Windows. Il modo più semplice per scriverlo è aprire l'header file e copiare l'intestazione della funzione, aggiungendo un nome ai due parametri.
    Di seguito comunque il codice completo (WindowsShell.c):

    Codice

    Il codice mostrato sopra usa delle strutture e delle funzioni della WINAPI (puro codice nativo insomma). La prima funzione ottiene l'handle dell'output standard (il terminale o prompt dei comandi), successivamente dichiara un riferimento alla struttura COORD che contiene appunto le coordinate X ed Y. Una volta ottenuto il buffer scrive N "spazi" e poi setta la posizione del cursore alle coordinate (0,0).

    Le funzioni più sotto eseguono circa le stesse operazioni, ma sono più semplici da comprendere. Ora che abbiamo anche l'implementazioni è necessario compilare il file. Questo passaggio è abbastanza intricato; io come sempre parto dall'assunto che il compilatore sia MinGW 32bit e che l'ambiente Java sia a 32bit (non siete costretti ad utilizzare Windows a 32bit, ma Java e MinGW dovranno esserlo; se uno di loro non lo fosse non riuscirete a creare la DLL).

    Iniziamo a compilare ed ottenere l'object file:
    CODICE
    gcc -c -o WindowsShell.o WindowsShell.c


    A questo punto dobbiamo creare la DLL (assicuratevi di avere l'header file generato con javah nella stessa directory):
    CODICE
    gcc -o WindowsShell.dll -s -shared WindowsShell.o -Wl,--kill-at


    (digitando gcc --help otterrete l'help sui comandi che stiamo utilizzando, altrimenti potete guardare sul sito di MinGW).

    Nella directory ora abbiamo la nostra DLL chiamata WindowsShell. A questo punto non ci resta che testare la prima libreria. L'utilizzo è molto semplice, più di quanto si possa credere: dovremo solo creare un'istanza di quella che è la nostra "interfaccia", ovvero la classe che definiva i metodi nativi, e poi richiamare il metodo desiderato. Ecco una classe di test (Example.java):
    CODICE
    import java.util.Scanner;

    class Example
    {
     public static void main(String[] args)
     {
       JavaWindowsShell jws = new JavaWindowsShell();
     
       Scanner s = new Scanner(System.in);
       s.useDelimiter("\r\n");
       
       while(true)
       {
         System.out.println("1. Clear Screen\n"+
                            "2. Set Color\n"+  
                            "3. Set Cursor position\n"
                           );
                           
         switch(s.nextInt())
         {
           case 1:
           jws.cls();
           break;
           case 2:
           jws.setColor((short)16,(short)20);
           break;
           case 3:
           jws.setCursorPosition((short)10,(short)10);
           
         }
       }
     }
    }




    Se digitiamo 1, otteniamo:



    Come vedete il nostro "clear screen" funziona!

    Hardware Information

    Ottenere informazioni sull'hardware in Java è assai complesso (anche se con le ultime versione qualche passo avanti è stato fatto; è però necessario ricordare che è pensato per essere multipiattaforma). Grazie alla nostra libreria però otterremo informazioni che solo un interfacciamento ad un livello così approfondito può darci!
    Come sempre iniziamo dalla classe che contiene i metodi nativi, WindowsHardwareInformation.java:

    Codice

    A differenza dell'esempio mostrato in precedenza, in questo vedremo come vengono trattati i tipi di dati tra C e Java, e come ottenere quindi una stringa accettabile in linguaggio C (per restituirla poi a Java, dato che uno dei nostri metodi restituisce String).

    Anche in questo caso, è necessario compilare...
    CODICE
    javac WindowsHardwareInformation.java


    ...ed ottenere il file header da usare nel futuro sorgente C:
    CODICE
    javah WindowsHardwareInformation


    Il file header che otteniamo questa volta è il seguente:

    Codice

    Di nuovo, attenzione all'header file: non compaiono int e String, ma jint e jstring. Questi sono proprio i dati che utilizzeremo nel sorgente C se operiamo con interi e stringhe (esistono ovviamente jshort, jdouble etc). La definizioni di questi tipi e di molte altre funzioni si trovano negli header file copiati in precedenza (la cui analisi va decisamente oltre gli scopi dell'articolo).

    Ora è necessario creare il file sorgente in C, WindowsHardwareInformation.c:

    Codice

    Anche in questo caso, usiamo la WINAPI. Precisamente la struttura SYSTEM_INFO, dichiarata nell'header file windows.h (le definizioni però le troverete in winbase.h, nella directory di MinGW include), ha la seguente struttura:

    Codice

    Tutti i campi sono facilmente accessibili come si nota dalla prima funzione di WindowsHardwareInformation.c; si tratta di chiamare GetSystemInfo passando come parametro un riferimento alla struttura sopra citata. Successivamente leggere il campo di interesse, restituendo il risultato. Prestate attenzione al dato utilizzato: è un DWORD, ovvero è un DOUBLE WORD, cioè 32bit (come int).

    La funzione più complicata è sicuramente l'ultima. Qui si nota la poca semplicità. Il compito lo si poteva assolvere in svariati metodi, ma ho scelto di creare l'array in ogni blocco dell'if; la funzione NewStringUTF riceve un array di caratteri (C-style) e restituisce un oggetto String.


    A questo punto si procede con la compilazione e il linking:
    CODICE
    gcc -c -o WindowsHardwareInformation.o WindowsHardwareInformation.c


    In questo caso compariranno dei messaggi "warning" a causa del cast... non li ho ancora corretti, tuttavia funziona.

    Creiamo quindi la DLL:

    CODICE
    gcc -o WindowsHardwareInformation.dll -s -shared WindowsHardwareInformation.o -Wl,--kill-at


    Creiamo sempre una classe di esempio:

    CODICE
    class Example
    {
     public static void main(String[] args)
     {
       WindowsHardwareInformation whi = new WindowsHardwareInformation();
       
       System.out.println("Number of Processor: "+whi.numberOfProcessor());
       System.out.println("Processor Type: "+whi.processorType());
       System.out.println("Processor Architecture: "+whi.processorArchitecture());
     }
    }


    Compiliamo ed eseguiamo, sempre con i soliti comandi (javac e java).
    L'output è il seguente (nel mio caso):
    CODICE
    Number of Processor: 8
    Processor Type: 586
    Processor Architecture: x86


    Anche in questo caso l'istanza la si crea allo stesso modo, ed il dato che torna è specificato nelle definizioni di WindowsHardwareInformation.java.


    Memory Information

    L'ultimo esempio che analizzeremo è relativo alla memoria (RAM). Anche in questo caso la WINAPI sarà la nostra fedele compagna di viaggio. Procediamo con la solita creazione del file contenente i metodi nativi, WindowsMemoryInformation.java:

    Codice

    Ora compiliamo:

    CODICE
    javac WindowsMemoryInformation.java


    E generiamo l'header file per il programma C:
    CODICE
    javah WindowsMemoryInformation


    Ecco l'header generato:
    Codice

    Nulla di nuovo, l'header si presenta molto simile ai precedenti già analizzati. Proseguiamo quindi con il definire le nostre funzione e creare quindi il file WindowsMemoryInformation.c:

    Codice

    In questo caso non vi è nulla di particolarmente complicato: la struttura utilizzata è MEMORYSTATUS e viene inizializzata dalla funzione della WINAPI GlobalMemoryStatus. Letto il campo di interesse, viene restituito il valore al chiamante.

    Passiamo sempre alla fase di compilazione e linking:
    CODICE
    gcc -c -o WindowsMemoryInformation.o WindowsMemoryInformation.c


    Ed ora creiamo la DLL:
    CODICE
    gcc -o WindowsMemoryInformation.dll -s -shared WindowsMemoryInformation.o -Wl,--kill-at


    Come prima, creiamo una piccola classe di test che utilizza la libreria:
    CODICE
    class Example
    {
     public static void main(String[] args)
     {
       WindowsMemoryInformation wmi = new WindowsMemoryInformation();
       System.out.println("Memory Length: "+wmi.length());
       System.out.println("Memory Load: "+wmi.memoryLoad());
       System.out.println("Total Memory Physics: "+wmi.totalPhys());
       System.out.println("Available Memory Physics: "+wmi.availPhys());
       System.out.println("Total Memory Virtual: "+wmi.totalVirtual());
       System.out.println("Available Memory Virtual: "+wmi.availVirtual());
     }
    }


    Compiliamo sempre con javac e poi eseguiamo con java, l'output è il seguente (nel mio caso):
    CODICE
    Memory Length: 32
    Memory Load: 51
    Total Memory Physics: 2147483647
    Available Memory Physics: 1389903872
    Total Memory Virtual: 2147352576
    Available Memory Virtual: 1696002048


    Una breve descrizione dei valori che vediamo:
    length è la lunghezza in byte della struttura MEMORYSTATUS;
    load, è un numero compreso tra 0 e 100 e specifica la percentuale di utilizzo della memoria;
    total physics, la memoria fisica totale (espresso in byte);
    available pysics indica la memoria attualmente disponibile in byts;
    total virtual indica la memoria virtuale disponibile per questo processo (2GB per i 32bit, approssimativamente);
    available virtual, indica la memoria attualmente non richiesta e non riservata

    Anche questa struttura come la precedente presenta altri campi.

    Utilizzare le tre librerie

    L'utilizzo delle tre librerie a questo punto è molto semplice. Tutto ciò che dobbiamo avere sono i file WindowsMemoryInformation.java, WindowsHardwareInformation.java e JavaWindowsShell.java, più le rispettive DLL tutte nella stessa directory (altrimenti sarà necessario specificare il percorso appropriato).

    Il file di test che ho creato si chiama SoftwareExample.java e banalmente chiede all'utente quale informazione mostrare a video; ogni volta che avviene una scelta, si verifica la pulizia della finestra (utilizzando la funzione della libreria scritta prima). Di seguito il codice:

    Codice

    Risulta evidente che la fase complessa è solo la scrittura delle DLL e - per chi non è abituato - la loro creazione. L'utilizzo è di per sé molto semplice e non differisce molto dalle normali classi Java. La creazione dell'istanza e la chiamata ai metodi avviene allo stesso modo: istanziando semplicemente la classe dei metodi nativi, e poi chiamare il metodo desiderato.

    Qui l'output dell'ultimo programma:




    Conclusione

    L'articolo termina qui, di seguito troverete uno Zip contenente le DLL ed i file sorgenti utilizzati. Come abbiamo visto, utilizzare codice nativo è possibile, anche se non propriamente semplice. Va da sé che in caso di applicazioni che necessitano di molte informazioni (accessi a codice nativo) e che girano su un solo sistema operativo... ci si potrebbe chiedere se Java è il linguaggio migliore, o se passare a C# (usando magari .Net) o C/C++ ed usare la WINAPI (in questo caso l'accesso alla WINAPI non varierà, ma ci si risparmierà i vari jint, jshort, jstring,... e le altre funzioni).

    Download Mediafire (Zip)


    Se avete domande postate qui sotto. ;)
    Grazie a tutti!

    P.S. un ultima cosa: può essere eseguito solo su un 32bit, sopra non sono stato molto chiaro. Non appena potrò magari scriverò del codice per il 64bit.

    Edited by JS96 - 6/9/2014, 11:32
     
    .
  2.      
     
    .
    Avatar

    Where there's a user input, there's a vulnerability.

    Group
    Manager
    Posts
    11,133
    Reputazione
    +174

    Status
    Ho letto tutto l'articolo, con una voce da video ahah. :)
    E' veramente sorprendente, fatto veramente bene (come tutti gli altri tuoi d'altronde)!
    Ho modificato gli errori trovati nel testo. :)
    Per il resto tutto perfetto! ;) Bravissimo!!

    CITAZIONE
    Questi sono proprio i dati che utilizzeremo nel sorgente C se operiamo con interi e stringhe (esistono ovviamente jshort, jdouble etc). La definizioni di questi tipi e di molte altre funzioni si trovano negli header file copiati in precedenza (la cui analisi va decisamente oltre gli scopi dell'articolo).

    Perché, appena puoi e vuoi, non fai un altro articolo da collegare a questo trattante l'argomento soprastante? :)
     
    .
  3. Sonny Moore
         
     
    .

    User deleted


    Congratulazioni.
    Articolo ottimo come sempre :)
     
    .
  4. JS96
         
     
    .

    User deleted


    Ah, è questo il tuo articolo "corto". :asd:
    Comunque ottimo come sempre! :D
     
    .
  5. Guglielmoqwerty
         
     
    .

    User deleted


    Bell'articolo, solo due cose:
    1)
    CITAZIONE
    Tra le domande più comuni quando si inizia a programmare in Java è sempre presente la seguente: è possibile usare dell'altro codice nei nostri programmi Java?

    In realtà credo che non venga in mente a nessuno. Penso che all'inizio non sia nemmeno pensabile utilizzare due linguaggi contemporaneamente. :P
    Inoltre se non ricordo male Java è l'unico che permette questa "unione". (o mi sbaglio)

    2) Più mirato ai lettori: per spostare il cursore in java non serve chiamare le api di sistema, esiste una classe apposita: java.awt.Robot

    Per il resto (quindi tutto) l'articolo è perfetto, anche più chiaro di quello di De Sio che la sbriga in 4 righe dicendo "tanto non vi servirà mai" :asd:
     
    .
  6.      
     
    .
    Avatar

    Senior Member

    Group
    Manager
    Posts
    10,796
    Reputazione
    +266

    Status
    Grazie a tutti! :) Aspetto il parere di Koskha anche, dato che era uno di quelli interessati all'articolo. :P

    @Guglielmoqwerty: ovviamente non parlo di chi inizia a programmare e sceglie Java come primo linguaggio, anche se la domanda a me è sorta dopo un po'. Se cerchi su Google qualcuno che si pone quella domanda c'è. :asd: E mi riferisco all'eseguire codice C utilizzando Java; ma ho trovato nel corso degli anni chi chiedeva se fosse possibile "mischiare" i linguaggi...

    Per quanto riguarda il punto 2, conosco la classe, ma non sapevo si potesse spostare il cursore nel prompt. E' questo che si può fare? Chiedo perchè leggendo la descrizione dei metodi mi sembra più rivolto all'utilizzo di una GUI che al terminale stesso.

    In effetti comunque non è tra le funzionalità più utilizzate come ho detto anche nell'articolo; in caso di funzionalità specifiche ci può stare, ma se si tratta di qualcosa di più corposo è meglio usare un altro linguaggio.

    @JS96: si, non mi sembra molto lungo dai...

    Grazie ancora a tutti! ;)
     
    .
  7.      
     
    .
    Avatar

    Where there's a user input, there's a vulnerability.

    Group
    Manager
    Posts
    11,133
    Reputazione
    +174

    Status
    CITAZIONE (RootkitNeo @ 6/9/2014, 13:59) 
    Grazie a tutti! :) Aspetto il parere di Koskha anche, dato che era uno di quelli interessati all'articolo. :P

    @Guglielmoqwerty: ovviamente non parlo di chi inizia a programmare e sceglie Java come primo linguaggio, anche se la domanda a me è sorta dopo un po'. Se cerchi su Google qualcuno che si pone quella domanda c'è. :asd: E mi riferisco all'eseguire codice C utilizzando Java; ma ho trovato nel corso degli anni chi chiedeva se fosse possibile "mischiare" i linguaggi...

    Per quanto riguarda il punto 2, conosco la classe, ma non sapevo si potesse spostare il cursore nel prompt. E' questo che si può fare? Chiedo perchè leggendo la descrizione dei metodi mi sembra più rivolto all'utilizzo di una GUI che al terminale stesso.

    In effetti comunque non è tra le funzionalità più utilizzate come ho detto anche nell'articolo; in caso di funzionalità specifiche ci può stare, ma se si tratta di qualcosa di più corposo è meglio usare un altro linguaggio.

    @JS96: si, non mi sembra molto lungo dai...

    Grazie ancora a tutti! ;)

    Mi hai totalmente ignorato... :asd:
     
    .
  8.      
     
    .
    Avatar

    Senior Member

    Group
    Manager
    Posts
    10,796
    Reputazione
    +266

    Status
    Hai ragione scusa, ho avuto un imprevisto e non ho aggiunto la risposta.

    Potrebbe essere un'idea l'articolo su tutte le altre cose, tuttavia devono venirmi idee carine su come presentare il tutto. ;)
    Grazie per le correzioni! :P
     
    .
  9.      
     
    .
    Avatar

    Senior Member

    Group
    Manager
    Posts
    10,796
    Reputazione
    +266

    Status
    @Alexander Cerutti: poi dovrai anche spiegarmi di preciso com'è la voce da video. :asd:

    E grazie per aver messo l'articolo in rilievo, lo apprezzo molto, mi fa piacere vedere che ciò che scrivo viene apprezzato. ;)
     
    .
  10.      
     
    .
    Avatar

    Where there's a user input, there's a vulnerability.

    Group
    Manager
    Posts
    11,133
    Reputazione
    +174

    Status
    CITAZIONE (RootkitNeo @ 6/9/2014, 17:37) 
    @Alexander Cerutti: poi dovrai anche spiegarmi di preciso com'è la voce da video. :asd:

    E grazie per aver messo l'articolo in rilievo, lo apprezzo molto, mi fa piacere vedere che ciò che scrivo viene apprezzato. ;)

    In pratica è una voce seria ma esperta, come una videoguida, non la voce di un boccia. :asd:
    Prego! Tu ti impegni senza essere ripagato pecuniariamente, ed è giusto che venga anche ripagato in qualche modo come le visite! ;)
     
    .
  11. Koskha
         
     
    .

    User deleted


    Fantastico :D
    Questa cosa può avere infinite applicazioni, ad esempio potrebbe essere la base per un packer per java ad alte prestazioni e più difficile da unpackare (anche se solo per windows purtroppo). Spetta solo alla fantasia del programmatore trovare lo scopo che più gli aggrada :asd:
     
    .
  12.      
     
    .
    Avatar

    Senior Member

    Group
    Manager
    Posts
    10,796
    Reputazione
    +266

    Status
    Grazie Koskha! :)

    Si, in pratica è proprio così. Ho utilizzato apposta la WinAPI al posto di limitarmi ad esempi relativi a "passa una variabile, esegui un calcolo e restituisci il valore". Così credo renda meglio l'idea sul potenziale che può avere.
     
    .
11 replies since 6/9/2014, 01:59   1489 views
  Share  
.
Top