2016-01-15 8 views
6

Sto lavorando su un'API Java in cui molti degli oggetti Java sono realmente wrapper per oggetti C++ equivalenti. Gli oggetti Java creano gli oggetti C++ e sono responsabili della loro liberazione quando non sono più necessari. Mi chiedo circa il miglior modello da utilizzare per questo, posso vedere due opzioni possibili:Miglior pattern JNI per il wrapping di oggetti C++?

  1. Construct oggetto C++ nel costruttore con una chiamata di metodo nativo statica e variabile finale per tenere la maniglia nativo.

    public abstract class NativeBackedObject1 implements java.lang.AutoCloseable { 
    
        protected final long _nativeHandle; 
        protected final AtomicBoolean _nativeOwner; 
    
        protected NativeBackedObject1(final long nativeHandle) { 
         this._nativeHandle = nativeHandle; 
         this._nativeOwner = new AtomicBoolean(true); 
        } 
    
        @Override 
        public close() { 
         if(_nativeOwner.copareAndSet(true, false)) { 
          disposeInternal(); 
         } 
        } 
    
        protected abstract void disposeInternal(); 
    } 
    
    public SomeFoo1 extends NativeBackendObject1 { 
        public SomeFoo1() { 
         super(newFoo()); 
        } 
    
        @Override 
        protected final void disposeInternal() { 
         //TODO: any local object specific cleanup 
         disposeInternal(_nativeHandle); 
        } 
    
        private native static long newFoo(); 
        private native disposeInternal(final long nativeHandle); 
    } 
    
  2. Costruisce oggetto C++ nel costruttore utilizzando un metodo di chiamata esempio nativo e una variabile non finale per tenere la maniglia nativo.

    public abstract class NativeBackedObject2 implements java.lang.AutoCloseable { 
        protected long _nativeHandle; 
        protected boolean _nativeOwner; 
    
        protected NativeBackedObject2() { 
         this._nativeHandle = 0; 
         this._nativeOwner = true; 
        } 
    
        @Override 
        public void close() { 
         synchronized(this) { 
          if(_nativeOwner && _nativeHandle != 0) { 
           disposeInternal(); 
           _nativeHandle = 0; 
           _nativeOwner = false; 
          } 
         } 
        } 
    
        protected abstract void disposeInternal(); 
    } 
    
    public SomeFoo2 extends NativeBackendObject2 { 
        public SomeFoo2() { 
         super(); 
         _nativeHandle = newFoo(); 
        } 
    
        @Override 
        protected final void disposeInternal() { 
         //TODO: any local object specific cleanup 
         disposeInternal(_nativeHandle); 
        } 
    
        private native long newFoo(); 
        private native disposeInternal(final long nativeHandle); 
    } 
    

Al momento mi sto pensando che (1) è l'approccio migliore, perché:

  • a. Significa che posso impostare _nativeHandle come immutabile (final). Quindi non ho bisogno di preoccuparmi dell'accesso simultaneo ad esso o di cambiamenti inaspettati (il codice è in realtà più complesso di questi esempi semplicistici).
  • b. A causa del costruttore, ho formalizzato nel progetto che qualsiasi sottoclasse di NativeBackedObject è il proprietario del rispettivo oggetto nativo (rappresentato da _nativeHandle), poiché non può essere costruito senza di esso.

Esistono vantaggi dell'approccio (2) su (1) o di eventuali problemi di approccio (1)?

ho potuto anche vedere un modello alternativo per avvicinarsi (1), chiamiamola approccio (3):

public abstract class NativeBackedObject3 implements java.lang.AutoCloseable { 
    protected final long _nativeHandle; 
    protected final AtomicBoolean _nativeOwner; 

    protected NativeBackedObject3() { 
     this._nativeHandle = newInternal(); 
     this._nativeOwner = new AtomicBoolean(true); 
    } 

    @Override 
    public close() { 
     if(_nativeOwner.copareAndSet(true, false)) { 
      disposeInternal(); 
     } 
    } 

    protected abstract long newInternal(); 
    protected abstract void disposeInternal(); 
} 

public SomeFoo3 extends NativeBackendObject3 { 
    public SomeFoo3() { 
     super(); 
    } 

    @Override 
    protected final void disposeInternal() { 
     //TODO: any local object specific cleanup 
     disposeInternal(_nativeHandle); 
    } 

    @Override 
    protected long newInternal() { 
     return newFoo(); 
    }; 

    private native long newFoo(); 
    private native disposeInternal(final long nativeHandle); 
} 

Il vantaggio di (3) sopra (1), è che posso tornare a un costruttore predefinito, che potrebbe aiutare a creare dei mock per i test, ecc. Il principale svantaggio è che non posso più passare parametri addizionali a newFoo().

Forse ci sono altri approcci che mi sono perso? Suggerimenti benvenuto ...

+0

Che tipo di ciclo di vita avrà il tuo NativeBackedObjects? –

+0

Il ciclo di vita sarà gestito manualmente dal consumatore dell'API, ho implementato AutoCloseable in modo che abbiano la possibilità di utilizzare try-with-resources per gestire la pulizia. – adamretter

+0

Stai per fare un multithreading? Sto anche cercando di capire il tuo "_nativeOwner" AtomicBoolean. Poiché si tratta di una variabile di istanza 'finale' di' NativeBackedObject' creata dal costruttore di quell'oggetto, a meno che non si disponga di codice aggiuntivo non mostrato che modifica ciò che si definisce come proprietà, non vedo alcuna necessità per tale booleano. L'attuale 'NativeBackedObject' ha ** il ** riferimento al tuo oggetto nativo, ottenuto assumendo tramite' new' nel tuo codice nativo. Dovresti passare la proprietà in giro, e lo eviterei se possibile. Sarebbe un incubo O & M. –

risposta

1

Hai provato SWIG (http://www.swig.org) che può generare wrapper Java di oggetti C++?

%typemap(javabody) SWIGTYPE %{ 
    private long swigCPtr; 
    protected boolean swigCMemOwn; 

    public $javaclassname(long cPtr, boolean cMemoryOwn) { 
     swigCMemOwn = cMemoryOwn; 
     swigCPtr = cPtr; 
    } 

    public static long getCPtr($javaclassname obj) { 
     return (obj == null) ? 0 : obj.swigCPtr; 
    } 
%} 

come documentazione per SWIG dice, considerano semplice classe di test:

class Test { 
    string str; 
public: 
    Test() : str("initial") {} 
}; 

l'uscita perché è:

public class Test { 
    private long swigCPtr; 
    protected boolean swigCMemOwn; 

    protected Test(long cPtr, boolean cMemoryOwn) { 
    swigCMemOwn = cMemoryOwn; 
    swigCPtr = cPtr; 
    } 

    protected static long getCPtr(Test obj) { 
    return (obj == null) ? 0 : obj.swigCPtr; 
    } 

    protected void finalize() { 
    delete(); 
    } 

    // Call C++ destructor 
    public synchronized void delete() { 
    if(swigCPtr != 0 && swigCMemOwn) { 
     swigCMemOwn = false; 
      exampleJNI.delete_Test(swigCPtr); 
     } 
     swigCPtr = 0; 
     } 

    // Call C++ constructor 
    public Test() { 
    this(exampleJNI.new_Test(), true); 
    } 

} 
+0

No. Sai che tipo di schema segue? – adamretter

+0

Grazie @ tomasz-jarosik che è molto interessante. Quindi sembra che SWIG stia facendo "By Call" dai pattern che ho documentato qui - https://github.com/adamretter/jni-construction-benchmark – adamretter

+1

Confronto cool! Penso che SWIG usi "By Call, static" perché hanno static ovunque. –

1

L'approccio "per chiamata, statico" è infatti più efficiente come da riferimento a https://github.com/adamretter/jni-construction-benchmark ma JavaCPP utilizza fondamentalmente "By Call, Invoke" (che BTW può essere reso un po m efficiente con la memorizzazione nella cache del jfieldID).

Il motivo per cui ho scelto di farlo in questo modo è produrre un file di interfaccia Java più pulito. L'utente può decidere di scriverlo manualmente o lasciare che lo strumento lo generi dai file di intestazione.Ad ogni modo, finisce per leggere come una traduzione Java di alcuni file header. Tuttavia, quanto più cruft aggiungiamo a tale interfaccia, tanto meno sembra C++, e tanto più difficile diventa scrivere o leggerlo. (Questa è una delle molte cose che non mi piacciono di SWIG.)

BTW, non è vietato modificare le variabili final da JNI, quindi potrebbe essere un'altra ragione per cui si potrebbe desiderare di farlo in questo modo.

Ovviamente, è ancora possibile modificare JavaCPP e supportare il modo più efficiente dal punto di vista computazionale di fare le cose, ma risparmiamo a malapena in qualsiasi momento che non si è ancora dimostrato un problema.

Problemi correlati