Пособие по JNI

В принципе java является самодостаточным языком. Но иногда требуется задействовать код, который на низком уровне общается со специфичным аппаратным обеспечением или имеется только на языке C/C++ и его портирование в java проблематично.
В данной заметке я рассмотрю использование JNI (Java Native Interface): мы вызовем в java классе методы, написанный на C, C++ и даже на ассемблере. А из собранной SO библиотеки (работа будет происходить в линукс) мы вызовем методы java класса.

Для упрощения сборки использовался makefile:

NAME = HelloJNI
LIB_NAME = libhello
 
all : class header so 
 
# $@ matches the target,
so : $(NAME).cpp $(NAME).h
gcc -o $(LIB_NAME).so -fPIC -lc -shared \
-I/usr/lib/jvm/java-1.6.0-openjdk-1.6.0.34.x86_64/include \
-I/usr/lib/jvm/java-1.6.0-openjdk-1.6.0.34.x86_64/include/linux $(NAME).cpp
 
# building java class, need java file
class : $(NAME).java
javac $(NAME).java
 
# $* matches the target filename without the extension
header : $(NAME).class
javah $(NAME)
 
clean :
rm $(NAME).h $(NAME).class $(LIB_NAME).so

Свой путь к хедеру jni.h (в моем случае это /usr/lib/jvm/java-1.6.0-openjdk-1.6.0.34.x86_64/include) можно найти командой:

find / -name jni.h

Второй путь образуется добавлением «/linux».

Сборка запускается командой:

make all

При этом сначала собирается java класс (javac HelloJNI.java), потом из него генерится заголовочный файл (javah HelloJNI) и, наконец, собирается SO библиотека из хедера и созданного вручную CPP файла.

Для корректного обнаружения собранной SO библиотеки нужно выполнить команду (подразумевается, что она находится в текущей папке):

export LD_LIBRARY_PATH=.

Для описания нативных методов в java существует ключевое слово native. Это будет означать, что JVM при вызове этого метода будет передавать управление нативному коду.

Затем, нам надо загрузить нативную библиотеку. Для этого можно вызвать System.loadLibrary(String), которая принимает в качестве параметра имя библиотеки. После этого вызова библиотека будет загружена в адресное пространство JVM.

Код в java классе для вызова С/С++/asm

public class HelloJNI {
   static {
      System.loadLibrary("hello"); // Load native library at runtime
                                   // hello.dll (Windows) or libhello.so (Unixes)
   }
 
   // Declare a native method sayHello() that receives nothing and returns void
   private native void sayHello();
 
   // Test Driver
   public static void main(String[] args) {
      new HelloJNI().sayHello();  // invoke the native method
   }
}

Для него будет сгенерирован такой заголовочный файл HelloJNI.h:

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class HelloJNI */
 
#ifndef _Included_HelloJNI
#define _Included_HelloJNI
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     HelloJNI
 * Method:    sayHello
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_HelloJNI_sayHello(JNIEnv *, jobject);
 
#ifdef __cplusplus
}
#endif
#endif
</jni.h>

Создаем HelloJNI.cpp:

#include <jni.h>
#include <stdio.h>
#include <iostream>
#include "HelloJNI.h"
 
using namespace std;
 
int gcd( int a, int b ) {
    int result ;
    /* Compute Greatest Common Divisor using Euclid's Algorithm */
    __asm__ __volatile__ ( "movl %1, %%eax;"
                          "movl %2, %%ebx;"
                          "CONTD: cmpl $0, %%ebx;"
                          "je DONE;"
                          "xorl %%edx, %%edx;"
                          "idivl %%ebx;"
                          "movl %%ebx, %%eax;"
                          "movl %%edx, %%ebx;"
                          "jmp CONTD;"
                          "DONE: movl %%eax, %0;" : "=g" (result) : "g" (a), "g" (b)
    );
 
    return result ;
}
 
// Implementation of native method sayHello() of HelloJNI class
JNIEXPORT void JNICALL Java_HelloJNI_sayHello(JNIEnv *env, jobject thisObj) {
   printf("Hello World!\n");
   cout &lt;&lt; "Hello World from C++!" &lt;&lt; endl;
 
   cout &lt;&lt; "Greatest common divisor of 60 and 36 is " &lt;&lt; gcd(60, 36) &lt;&lt; endl;
 
   return;
}
</iostream></stdio.h></jni.h>

Непосредственно в java классе вызывается метод sayHello(). В нем происходит вызов функции из C – printf, функции из С++ cout и функции с ассемблерной вставкой. В последней реализован поиск наибольшего общего делителя двух целых чисел. Вы можете вставить туда код общения с вашим устройством сбора данных, которое работает через LPT порт 🙂

Передача в нативный код простых типов данных

Вставка в java:

   // Declare a native method average() that receives two ints and return a double containing the average
   private native double average(int n1, int n2);
 
   // Test Driver
   public static void main(String args[]) {
      System.out.println("In Java, the average is [" + new HelloJNI().average(3, 2) + "]");
   }

Реализация в CPP:

JNIEXPORT jdouble JNICALL Java_HelloJNI_average
          (JNIEnv *env, jobject thisObj, jint n1, jint n2) {
   jdouble result;
   printf("In C, the numbers are %d and %d\n", n1, n2);
   result = ((jdouble)n1 + n2) / 2.0;
   // jint is mapped to int, jdouble is mapped to double
   return result;
}

Название функции Java_HelloJNI_average состоит из неизменяемой приставки Java, названия класса HelloJNI и названия метода average.

Отображение типов между java и cpp такое:

Определение Тип
typedef unsigned char Boolean
typedef unsigned short Jchar
typedef short Jshort
typedef float Jfloat
typedef double Jdouble
typedef long Jint
typedef __int64 Jlong
typedef signed char Jbyte

Передача строк

Вставка в java:

   // Native method that receives a Java String and return a Java String
   private native String say(String msg);
 
   public static void main(String args[]) {
      String result = new HelloJNI().say("Hello from Java");
      System.out.println("In Java, the returned string is: " + result);
   }

Реализация в СРР:

JNIEXPORT jstring JNICALL Java_HelloJNI_say(JNIEnv *env, jobject thisObj, jstring inJNIStr) {
   // Step 1: Convert the JNI String (jstring) into C-String (char*)
   const char *inCStr = env-&gt;GetStringUTFChars(inJNIStr, NULL);
   if (NULL == inCStr) return NULL;
 
   // Step 2: Perform its intended operations
   printf("In C, the received string is: %s\n", inCStr);
   env-&gt;ReleaseStringUTFChars( inJNIStr, inCStr);  // release resources
 
   // Prompt user for a C-string
   char outCStr[128];
   printf("Enter a String: ");
   scanf("%s", outCStr);    // not more than 127 characters
 
   // Step 3: Convert the C-string (char*) into JNI String (jstring) and return
   return env-&gt;NewStringUTF( outCStr);
}

Обратите внимание на то, что в случае использования расширения CPP обращения должны быть написаны в стиле С++: env->GetStringUTFChars(). В случае С это будет выглядеть так: (*env)->GetStringUTFChars. Если этого не сделать, появятся такие ошибки: base operand of ‘->’ has non-pointer type ‘JNIEnv_’. Еще одно изменение по сравнению со статьями, которые я нашел по теме: сигнатура вызовов. Мне пришлось везде удалять первый аргумент — env.

Передача массива переменных примитивных типов

Вставка в java:

   // Declare a native method sumAndAverage() that receives an int[] and
   //  return a double[2] array with [0] as sum and [1] as average
   private native double[] sumAndAverage(int[] numbers);
 
   // Test Driver
   public static void main(String args[]) {
      int[] numbers = {22, 33, 33};
      double[] results = new HelloJNI().sumAndAverage(numbers);
      System.out.println("In Java, the sum is " + results[0]);
      System.out.println("In Java, the average is " + results[1]);
   }

Реализация в СРР:

JNIEXPORT jdoubleArray JNICALL Java_HelloJNI_sumAndAverage
          (JNIEnv *env, jobject thisObj, jintArray inJNIArray) {
   // Step 1: Convert the incoming JNI jintarray to C's jint[]
   jint *inCArray = env-&gt;GetIntArrayElements( inJNIArray, NULL);
   if (NULL == inCArray) return NULL;
   jsize length = env-&gt;GetArrayLength( inJNIArray);
 
   // Step 2: Perform its intended operations
   jint sum = 0;
   int i;
   for (i = 0; i &lt; length; i++) {
      sum += inCArray[i];
   }
   jdouble average = (jdouble)sum / length;
   env-&gt;ReleaseIntArrayElements( inJNIArray, inCArray, 0); // release resources
 
   jdouble outCArray[] = {sum, average};
 
   // Step 3: Convert the C's Native jdouble[] to JNI jdoublearray, and return
   jdoubleArray outJNIArray = env-&gt;NewDoubleArray( 2);  // allocate
   if (NULL == outJNIArray) return NULL;
   env-&gt;SetDoubleArrayRegion( outJNIArray, 0 , 2, outCArray);  // copy
   return outJNIArray;
}

Получение доступа к данным класса

Вставка в java:

   // Instance variables
   private int number = 88;
   private String message = "Hello from Java";
 
   // Declare a native method that modifies the instance variables
   private native void modifyInstanceVariable();
 
   // Test Driver   
   public static void main(String args[]) {
      HelloJNI test = new HelloJNI();
      test.modifyInstanceVariable();
      System.out.println("In Java, int is " + test.number);
      System.out.println("In Java, String is " + test.message);
   }

Реализация в СРР:

JNIEXPORT void JNICALL Java_HelloJNI_modifyInstanceVariable
          (JNIEnv *env, jobject thisObj) {
   // Get a reference to this object's class
   jclass thisClass = env-&gt;GetObjectClass( thisObj);
 
   // int
   // Get the Field ID of the instance variables "number"
   jfieldID fidNumber = env-&gt;GetFieldID( thisClass, "number", "I");
   if (NULL == fidNumber) return;
 
   // Get the int given the Field ID
   jint number = env-&gt;GetIntField( thisObj, fidNumber);
   printf("In C, the int is %d\n", number);
 
   // Change the variable
   number = 99;
   env-&gt;SetIntField( thisObj, fidNumber, number);
 
   // Get the Field ID of the instance variables "message"
   jfieldID fidMessage = env-&gt;GetFieldID( thisClass, "message", "Ljava/lang/String;");
   if (NULL == fidMessage) return;
 
   // String
   // Get the object given the Field ID
   jstring message = (jstring) env-&gt;GetObjectField( thisObj, fidMessage);
 
   // Create a C-string with the JNI String
   const char *cStr = env-&gt;GetStringUTFChars( message, NULL);
   if (NULL == cStr) return;
 
   printf("In C, the string is %s\n", cStr);
   env-&gt;ReleaseStringUTFChars( message, cStr);
 
   // Create a new C-string and assign to the JNI string
   message = env-&gt;NewStringUTF( "Hello from C");
   if (NULL == message) return;
 
   // modify the instance variables
   env-&gt;SetObjectField( thisObj, fidMessage, message);
}

Изменение значения статической переменной

Вставка в java:

   // Static variables
   private static double number = 55.66;
 
   // Declare a native method that modifies the static variable
   private native void modifyStaticVariable();
 
   // Test Driver
   public static void main(String args[]) {
      HelloJNI test = new HelloJNI();
      test.modifyStaticVariable();
      System.out.println("In Java, the double is " + number);
   }

Реализация в СРР:

JNIEXPORT void JNICALL Java_HelloJNI_modifyStaticVariable
          (JNIEnv *env, jobject thisObj) {
   // Get a reference to this object's class
   jclass cls = env-&gt;GetObjectClass( thisObj);
 
   // Read the int static variable and modify its value
   jfieldID fidNumber = env-&gt;GetStaticFieldID( cls, "numberDouble", "D");
   if (NULL == fidNumber) return;
   jdouble number = env-&gt;GetStaticDoubleField( cls, fidNumber);
   printf("In C, the double is %f\n", number);
   number = 99.88;
   env-&gt;SetStaticDoubleField( cls, fidNumber, number);
}

Функции обратного вызова (callback) и статические методы

Вставка в java:

   // Declare a native method that calls back the Java methods below
   private native void nativeMethod();
 
   // To be called back by the native code
   private void callback() {
      System.out.println("In Java");
   }
 
   private void callback(String message) {
      System.out.println("In Java with " + message);
   }
 
   private double callbackAverage(int n1, int n2) {
      return ((double)n1 + n2) / 2.0;
   }
 
   // Static method to be called back
   private static String callbackStatic() {
      return "From static Java method";
   }
 
   // Test Driver 
   public static void main(String args[]) {
      new HelloJNI().nativeMethod();
   }

Класс объявляет нативный метод nativeMethod() и вызывает его. В свою очередь, этот метод вызывает статические и нестатические методы класса.

Реализация в СРР:

JNIEXPORT void JNICALL Java_HelloJNI_nativeMethod
          (JNIEnv *env, jobject thisObj) {
 
   // Get a class reference for this object
   jclass thisClass = env-&gt;GetObjectClass( thisObj);
 
   // Get the Method ID for method "callback", which takes no arg and return void
   jmethodID midCallBack = env-&gt;GetMethodID( thisClass, "callback", "()V");
   if (NULL == midCallBack) return;
   printf("In C, call back Java's callback()\n");
   // Call back the method (which returns void), baed on the Method ID
   env-&gt;CallVoidMethod( thisObj, midCallBack);
 
   jmethodID midCallBackStr = env-&gt;GetMethodID( thisClass,
                               "callback", "(Ljava/lang/String;)V");
   if (NULL == midCallBackStr) return;
   printf("In C, call back Java's called(String)\n");
   jstring message = env-&gt;NewStringUTF( "Hello from C");
   env-&gt;CallVoidMethod( thisObj, midCallBackStr, message);
 
   jmethodID midCallBackAverage = env-&gt;GetMethodID( thisClass,
                                  "callbackAverage", "(II)D");
   if (NULL == midCallBackAverage) return;
   jdouble average = env-&gt;CallDoubleMethod( thisObj, midCallBackAverage, 2, 3);
   printf("In C, the average is %f\n", average);
 
   jmethodID midCallBackStatic = env-&gt;GetStaticMethodID( thisClass,
                                 "callbackStatic", "()Ljava/lang/String;");
   if (NULL == midCallBackStatic) return;
   jstring resultJNIStr = (jstring) env-&gt;CallStaticObjectMethod( thisClass, midCallBackStatic);
   const char *resultCStr = env-&gt;GetStringUTFChars( resultJNIStr, NULL);
   if (NULL == resultCStr) return;
   printf("In C, the returned string is %s\n", resultCStr);
   env-&gt;ReleaseStringUTFChars( resultJNIStr, resultCStr);
}

Для вызова нестатического метода класса нужно:
1. Получить ссылку на объект вызовом GetObjectClass().
2. Из ссылки на класс получить Method ID вызовом GetMethodID(). Нужно предоставить имя и сигнатуру. Сигнатура должна быть в виде «(parameters)return-type». Сигнатуру можно получить с помощью утилиты javap (Class File Disassembler) с аргументами -s (print signature) and -p (show private members):

# javap --help
# javap -s -p HelloJNI
  .......
  private void callback();
    Signature: ()V
 
  private void callback(java.lang.String);
    Signature: (Ljava/lang/String;)V
 
  private double callbackAverage(int, int);
    Signature: (II)D
 
  private static java.lang.String callbackStatic();
    Signature: ()Ljava/lang/String;

Создание объекта в нативном коде с помощью переданного коллбека

Вставка в java:

   // Native method that calls back the constructor and return the constructed object.
   // Return an Integer object with the given int.
   private native Integer getIntegerObject(int number);
 
   public static void main(String args[]) {
      TestJNIConstructor obj = new HelloJNI();
      System.out.println("In Java, the number is :" + obj.getIntegerObject(9999));
   }

Класс объявляет нативный метод getIntegerObject(). В С++ коде мы создадим и вернем Integer, который примет переданный параметр в конструкторе.

Реализация в СРР:

JNIEXPORT jobject JNICALL Java_HelloJNI_getIntegerObject
          (JNIEnv *env, jobject thisObj, jint number) {
   // Get a class reference for java.lang.Integer
   jclass cls = env-&gt;FindClass( "java/lang/Integer");
 
   // Get the Method ID of the constructor which takes an int
   jmethodID midInit = env-&gt;GetMethodID( cls, "<init>", "(I)V");
   if (NULL == midInit) return NULL;
   // Call back constructor to allocate a new instance, with an int argument
   jobject newObj = env-&gt;NewObject( cls, midInit, number);
 
   // Try runnning the toString() on this newly create object
   jmethodID midToString = env-&gt;GetMethodID( cls, "toString", "()Ljava/lang/String;");
   if (NULL == midToString) return NULL;
   jstring resultStr = (jstring) env-&gt;CallObjectMethod( newObj, midToString);
   const char *resultCStr = env-&gt;GetStringUTFChars( resultStr, NULL);
   printf("In C: the number is %s\n", resultCStr);
 
   return newObj;
}
</init>

Работа с массивом объектов

Вставка в java:

   // Native method that receives an Integer[] and
   //  returns a Double[2] with [0] as sum and [1] as average
   private native Double[] sumAndAverageInt(Integer[] numbers); 
 
   public static void main(String[] args) {
      Integer[] numbersInt = {11, 22, 32};  // auto-box
      Double[] resultsDouble = new HelloJNI().sumAndAverageInt(numbersInt);
      System.out.println("In Java, the sum is " + resultsDouble[0]);  // auto-unbox
      System.out.println("In Java, the average is " + resultsDouble[1]);
   }

Класс объявляет нативный метод, который принимает массив Integer-ов, в нативном коде считаем их сумму и среднее, возвращает их в массиве. Массив объектов сначала передается в нативный код, а потом другой массив оттуда возвращается.

Реализация в СРР:

JNIEXPORT jobjectArray JNICALL Java_HelloJNI_sumAndAverageInt
          (JNIEnv *env, jobject thisObj, jobjectArray inJNIArray) {
   // Get a class reference for java.lang.Integer
   jclass classInteger = env-&gt;FindClass( "java/lang/Integer");
   // Use Integer.intValue() to retrieve the int
   jmethodID midIntValue = env-&gt;GetMethodID( classInteger, "intValue", "()I");
   if (NULL == midIntValue) return NULL;
 
   // Get the value of each Integer object in the array
   jsize length = env-&gt;GetArrayLength( inJNIArray);
   jint sum = 0;
   int i;
   for (i = 0; i &lt; length; i++) {
      jobject objInteger = env-&gt;GetObjectArrayElement( inJNIArray, i);
      if (NULL == objInteger) return NULL;
      jint value = env-&gt;CallIntMethod( objInteger, midIntValue);
      sum += value;
   }
   double average = (double)sum / length;
   printf("In C, the sum is %d\n", sum);
   printf("In C, the average is %f\n", average);
 
   // Get a class reference for java.lang.Double
   jclass classDouble = env-&gt;FindClass( "java/lang/Double");
 
   // Allocate a jobjectArray of 2 java.lang.Double
   jobjectArray outJNIArray = env-&gt;NewObjectArray( 2, classDouble, NULL);
 
   // Construct 2 Double objects by calling the constructor
   jmethodID midDoubleInit = env-&gt;GetMethodID( classDouble, "<init>", "(D)V");
   if (NULL == midDoubleInit) return NULL;
   jobject objSum = env-&gt;NewObject( classDouble, midDoubleInit, (double)sum);
   jobject objAve = env-&gt;NewObject( classDouble, midDoubleInit, average);
   // Set to the jobjectArray
   env-&gt;SetObjectArrayElement( outJNIArray, 0, objSum);
   env-&gt;SetObjectArrayElement( outJNIArray, 1, objAve);
   return outJNIArray;
}
</init>

Обратите внимание на то, что обработка массива объектов отличается от обработки массива данных примитивных типов: нужно использовать Get|SetObjectArrayElement() для обработки каждого элемента.

Работа с локальными и глобальными ссылками

Управление ссылками важно для написания эффективного кода. Например, мы часто используем FindClass(), GetMethodID(), GetFieldID() для получения jclass, jmethodID и jfieldID в нативном коде. Код можно сделать повторно используемым: значения можно получить один раз и закешировать.

JNI делит ссылки на объекты на две категории: локальные и глобальные:

1. Локальная ссылка создается в нативном методе и высвобождается (исчезает) при выходе из области видимости метода. Ее можно удалить явно вызовом DeleteLocalRef(). Объекты передаются в нативный метод как локальные ссылки. Все Java объекты (jobject) возвращаемые вызовами JNI функций являются локальными ссылками.
2. Глобальная ссылка существует до тех пор, пока не будут явно удалена программистом с помощью вызова DeleteGlobalRef(). Можно создать глобальную ссылку из локальной с помощью вызова NewGlobalRef().

Вставка в java:

   // A native method that returns a java.lang.Integer with the given int.
   private native Integer getIntegerObject1(int number);
 
   // Another native method that also returns a java.lang.Integer with the given int.
   private native Integer anotherGetIntegerObject(int number);
 
   public static void main(String args[]) {
      HelloJNI test = new HelloJNI();
      System.out.println(test.getIntegerObject(1));
      System.out.println(test.getIntegerObject(2));
      System.out.println(test.anotherGetIntegerObject(11));
      System.out.println(test.anotherGetIntegerObject(12));
      System.out.println(test.getIntegerObject(3));
      System.out.println(test.anotherGetIntegerObject(13));
   }

Реализация в СРР:

// Global Reference to the Java class "java.lang.Integer"
static jclass classInteger;
static jmethodID midIntegerInit;
 
jobject getInteger(JNIEnv *env, jobject thisObj, jint number) {
 
   // Get a class reference for java.lang.Integer if missing
   if (NULL == classInteger) {
      printf("Find java.lang.Integer\n");
      classInteger = (jclass) env-&gt;NewGlobalRef(env-&gt;FindClass( "java/lang/Integer"));
   }
   if (NULL == classInteger) return NULL;
 
   // Get the Method ID of the Integer's constructor if missing
   if (NULL == midIntegerInit) {
      printf("Get Method ID for java.lang.Integer's constructor\n");
      midIntegerInit = env-&gt;GetMethodID( classInteger, "<init>", "(I)V");
   }
   if (NULL == midIntegerInit) return NULL;
 
   // Call back constructor to allocate a new instance, with an int argument
   jobject newObj = env-&gt;NewObject( classInteger, midIntegerInit, number);
   printf("In C, constructed java.lang.Integer with number %d\n", number);
   return newObj;
}
 
JNIEXPORT jobject JNICALL Java_HelloJNI_getIntegerObject1
          (JNIEnv *env, jobject thisObj, jint number) {
   return getInteger(env, thisObj, number);
}
 
JNIEXPORT jobject JNICALL Java_HelloJNI_anotherGetIntegerObject
          (JNIEnv *env, jobject thisObj, jint number) {
   return getInteger(env, thisObj, number);
}
</init>

Обратите внимание: FindClass() возвращает локальную ссылку и ее нужно явно преобразовать в глобальную:

classInteger = (jclass) env-&gt;NewGlobalRef(env-&gt;FindClass( "java/lang/Integer"));

Если этого не сделать, то программа аварийно завершается сбросом корки.

Вывод программы для всех реализованных случаев:

# java HelloJNI
Hello World!
Hello World from C++!
Greatest common divisor of 60 and 36 is 12
******************
In C, the numbers are 3 and 2
In Java, the average is [2.5]
******************
In C, the received string is: Hello from Java]
Enter a String: sdf
In Java, the returned string is: [sdf]
******************
In Java, the sum is [88.0]
In Java, the average is [29.333333333333332]
******************
In C, the int is 88
In C, the string is Hello from Java
In Java, int is [99]
In Java, String is [Hello from C]
******************
In C, the double is 77.660000
In Java, the double is 99.88
******************
In C, call back Java's callback()
In Java: callback() called
In C, call back Java's called(String)
In Java (callback) with [Hello from C]
In C, the average is 2.500000
In C, the returned string is From callbackStatic() Java method
In C: the number is 9999
In Java, the number is :9999
******************
In C, the sum is 65
In C, the average is 21.666667
In Java, the sum is 65.0
In Java, the average is 21.666666666666668
******************
Find java.lang.Integer
Get Method ID for java.lang.Integer's constructor
In C, constructed java.lang.Integer with number 1
1
In C, constructed java.lang.Integer with number 2
2
In C, constructed java.lang.Integer with number 11
11
In C, constructed java.lang.Integer with number 12
12
In C, constructed java.lang.Integer with number 3
3
In C, constructed java.lang.Integer with number 13
13

На этом обзор JNI закончен.

Использовались статьи: первая вторая

You can leave a response, or trackback from your own site.

Leave a Reply