Home | History | Annotate | Download | only in jni
      1 /*
      2  * Copyright (C) 2018 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 #include <atomic>
     18 #include <inttypes.h>
     19 #include <stdio.h>
     20 #include <string>
     21 #include <thread>
     22 #include <time.h>
     23 #include <vector>
     24 
     25 //#define LOG_NDEBUG 0
     26 #define LOG_TAG "NativeMidiManager-JNI"
     27 #include <android/log.h>
     28 
     29 #include <jni.h>
     30 
     31 #include <amidi/AMidi.h>
     32 
     33 extern "C" {
     34 
     35 /*
     36  * Structures for storing data flowing through the echo server.
     37  */
     38 #define SIZE_DATABUFFER 256
     39 /*
     40  * Received Messages
     41  */
     42 typedef struct {
     43     std::unique_ptr<uint8_t[]> dataBuff;
     44     size_t numDataBytes;
     45     int32_t opCode;
     46     int64_t timestamp;
     47     int64_t timeReceived;
     48 } ReceivedMessageRecord;
     49 
     50 /*
     51  * Sent Messages
     52  */
     53 typedef struct {
     54     std::unique_ptr<uint8_t[]> dataBuff;
     55     size_t numDataBytes;
     56     int64_t timestamp;
     57     long timeSent;
     58 } SentMessageRecord;
     59 
     60 /*
     61  * Context
     62  * Holds the state of a given test and native MIDI I/O setup for that test.
     63  * NOTE: There is one of these per test (and therefore unique to each test thread).
     64  */
     65 class TestContext {
     66 private:
     67     // counters
     68     std::atomic<int> mNumSends;
     69     std::atomic<int> mNumBytesSent;
     70     std::atomic<int> mNumReceives;
     71     std::atomic<int> mNumBytesReceived;
     72 
     73     std::vector<ReceivedMessageRecord> mReceivedMsgs;
     74     std::vector<SentMessageRecord> mSentMsgs;
     75 
     76     // Java NativeMidiMessage class stuff, for passing messages back out to the Java client.
     77     jclass mClsNativeMidiMessage;
     78     jmethodID mMidNativeMidiMessage_ctor;
     79     jfieldID mFid_opcode;
     80     jfieldID mFid_buffer;
     81     jfieldID mFid_len;
     82     jfieldID mFid_timestamp;
     83     jfieldID mFid_timeReceived;
     84 
     85     std::mutex lock;
     86 
     87 public:
     88     // read Thread
     89     std::unique_ptr<std::thread> mReadThread;
     90     std::atomic<bool> mReading;
     91 
     92     AMidiDevice* nativeDevice;
     93     std::atomic<AMidiOutputPort*> midiOutputPort;
     94     std::atomic<AMidiInputPort*> midiInputPort;
     95 
     96     TestContext() :
     97         mNumSends(0),
     98         mNumBytesSent(0),
     99         mNumReceives(0),
    100         mNumBytesReceived(0),
    101         mClsNativeMidiMessage(0),
    102         mMidNativeMidiMessage_ctor(0),
    103         mFid_opcode(0),
    104         mFid_buffer(0),
    105         mFid_len(0),
    106         mFid_timestamp(0),
    107         mFid_timeReceived(0),
    108         mReading(false),
    109         nativeDevice(nullptr),
    110         midiOutputPort(nullptr),
    111         midiInputPort(nullptr)
    112     {}
    113 
    114     bool initN(JNIEnv* env);
    115 
    116     int getNumSends() { return mNumSends; }
    117     void incNumSends() { mNumSends++; }
    118 
    119     int getNumBytesSent() { return mNumBytesSent; }
    120     void incNumBytesSent(int numBytes) { mNumBytesSent += numBytes; }
    121 
    122     int getNumReceives() { return mNumReceives; }
    123     void incNumReceives() { mNumReceives++; }
    124 
    125     int getNumBytesReceived() { return mNumBytesReceived; }
    126     void incNumBytesReceived(int numBytes) { mNumBytesReceived += numBytes; }
    127 
    128     void addSent(SentMessageRecord&& msg) { mSentMsgs.push_back(std::move(msg)); }
    129     size_t getNumSentMsgs() { return mSentMsgs.size(); }
    130 
    131     void addReceived(ReceivedMessageRecord&& msg) { mReceivedMsgs.push_back(std::move(msg)); }
    132     size_t getNumReceivedMsgs() { return mReceivedMsgs.size(); }
    133 
    134     jobject transferReceiveMsgAt(JNIEnv* env, int index);
    135 
    136     static const int COMPARE_SUCCESS = 0;
    137     static const int COMPARE_COUNTMISSMATCH = 1;
    138     static const int COMPARE_DATALENMISMATCH = 2;
    139     static const int COMPARE_DATAMISMATCH = 3;
    140     static const int COMPARE_TIMESTAMPMISMATCH = 4;
    141     int compareInsAndOuts();
    142 
    143     static const int CHECKLATENCY_SUCCESS = 0;
    144     static const int CHECKLATENCY_COUNTMISSMATCH = 1;
    145     static const int CHECKLATENCY_LATENCYEXCEEDED = 2;
    146     int checkInOutLatency(long maxLatencyNanos);
    147 };
    148 
    149 //
    150 // Helpers
    151 //
    152 static long System_nanoTime() {
    153     // this code is the implementation of System.nanoTime()
    154     // from system/code/ojluni/src/main/native/System.
    155     struct timespec now;
    156     clock_gettime(CLOCK_MONOTONIC, &now);
    157     return now.tv_sec * 1000000000LL + now.tv_nsec;
    158 }
    159 
    160 bool TestContext::initN(JNIEnv* env) {
    161     static const char* clsSigNativeMidiMessage = "android/nativemidi/cts/NativeMidiMessage";
    162 
    163     jclass cls = env->FindClass(clsSigNativeMidiMessage);
    164     if (cls == NULL) {
    165         __android_log_print(ANDROID_LOG_ERROR, LOG_TAG,
    166                 "JNI Error - couldn't find NativeMidiMessage class");
    167         return false; // we are doomed, so bail.
    168     }
    169     mClsNativeMidiMessage = (jclass)env->NewGlobalRef(cls);
    170     if (mClsNativeMidiMessage == NULL) {
    171         __android_log_print(ANDROID_LOG_ERROR, LOG_TAG,
    172                 "JNI Error - couldn't allocate NativeMidiMessage");
    173         return false; // we are doomed, so bail.
    174     }
    175 
    176     mMidNativeMidiMessage_ctor = env->GetMethodID(mClsNativeMidiMessage, "<init>", "()V");
    177     mFid_opcode = env->GetFieldID(mClsNativeMidiMessage, "opcode", "I");
    178     mFid_buffer = env->GetFieldID(mClsNativeMidiMessage, "buffer", "[B");
    179     mFid_len = env->GetFieldID( mClsNativeMidiMessage, "len", "I");
    180     mFid_timestamp = env->GetFieldID(mClsNativeMidiMessage, "timestamp", "J");
    181     mFid_timeReceived = env->GetFieldID(mClsNativeMidiMessage, "timeReceived", "J");
    182     if (mMidNativeMidiMessage_ctor == NULL ||
    183         mFid_opcode == NULL ||
    184         mFid_buffer == NULL ||
    185         mFid_len == NULL ||
    186         mFid_timestamp == NULL ||
    187         mFid_timeReceived == NULL) {
    188         __android_log_print(ANDROID_LOG_ERROR, LOG_TAG,
    189                 "JNI Error - couldn't load Field IDs");
    190         return false; // we are doomed, so bail.
    191     }
    192 
    193     return true;
    194 }
    195 
    196 jobject TestContext::transferReceiveMsgAt(JNIEnv* env, int index) {
    197     jobject msg = NULL;
    198 
    199     if (index < (int)mReceivedMsgs.size()) {
    200         ReceivedMessageRecord receiveRec = std::move(mReceivedMsgs.at(index));
    201         msg = env->NewObject(mClsNativeMidiMessage, mMidNativeMidiMessage_ctor);
    202 
    203         env->SetIntField(msg, mFid_opcode, receiveRec.opCode);
    204         env->SetIntField(msg, mFid_len, receiveRec.numDataBytes);
    205         jobject buffer_array = env->GetObjectField(msg, mFid_buffer);
    206         env->SetByteArrayRegion(reinterpret_cast<jbyteArray>(buffer_array), 0,
    207                 receiveRec.numDataBytes, (jbyte*)receiveRec.dataBuff.get());
    208         env->SetLongField(msg, mFid_timestamp, receiveRec.timestamp);
    209         env->SetLongField(msg, mFid_timeReceived, receiveRec.timeReceived);
    210     }
    211 
    212     return msg;
    213 }
    214 
    215 int TestContext::compareInsAndOuts() {
    216     // Number of messages sent/received
    217     if (mReceivedMsgs.size() != mSentMsgs.size()) {
    218         __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, "---- COMPARE_COUNTMISSMATCH r:%zu s:%zu",
    219                 mReceivedMsgs.size(), mSentMsgs.size());
    220        return COMPARE_COUNTMISSMATCH;
    221     }
    222 
    223     // we know that both vectors have the same number of messages from the test above.
    224     size_t numMessages = mSentMsgs.size();
    225     for (size_t msgIndex = 0; msgIndex < numMessages; msgIndex++) {
    226         // Data Length?
    227         if (mReceivedMsgs[msgIndex].numDataBytes != mSentMsgs[msgIndex].numDataBytes) {
    228             __android_log_print(ANDROID_LOG_ERROR, LOG_TAG,
    229                     "---- COMPARE_DATALENMISMATCH r:%zu s:%zu",
    230                     mReceivedMsgs[msgIndex].numDataBytes, mSentMsgs[msgIndex].numDataBytes);
    231             return COMPARE_DATALENMISMATCH;
    232         }
    233 
    234         // Timestamps
    235         if (mReceivedMsgs[msgIndex].timestamp != mSentMsgs[msgIndex].timestamp) {
    236             __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, "---- COMPARE_TIMESTAMPMISMATCH");
    237             return COMPARE_TIMESTAMPMISMATCH;
    238         }
    239 
    240         // we know that the data in both messages have the same number of bytes from the test above.
    241         int dataLen = mReceivedMsgs[msgIndex].numDataBytes;
    242         for (int dataIndex = 0; dataIndex < dataLen; dataIndex++) {
    243             // Data Values?
    244             if (mReceivedMsgs[msgIndex].dataBuff[dataIndex] !=
    245                     mSentMsgs[msgIndex].dataBuff[dataIndex]) {
    246                 __android_log_print(ANDROID_LOG_ERROR, LOG_TAG,
    247                         "---- COMPARE_DATAMISMATCH r:%d s:%d",
    248                         (int)mReceivedMsgs[msgIndex].dataBuff[dataIndex],
    249                         (int)mSentMsgs[msgIndex].dataBuff[dataIndex]);
    250                 return COMPARE_DATAMISMATCH;
    251             }
    252         }
    253     }
    254 
    255     return COMPARE_SUCCESS;
    256 }
    257 
    258 int TestContext::checkInOutLatency(long maxLatencyNanos) {
    259     if (mReceivedMsgs.size() != mSentMsgs.size()) {
    260         __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, "  ---- CHECKLATENCY_COUNTMISSMATCH");
    261         return CHECKLATENCY_COUNTMISSMATCH;
    262     }
    263 
    264     // we know that both vectors have the same number of messages
    265     // from the test above.
    266     int numMessages = mSentMsgs.size();
    267     for (int msgIndex = 0; msgIndex < numMessages; msgIndex++) {
    268         long timeDelta =  mSentMsgs[msgIndex].timeSent - mReceivedMsgs[msgIndex].timestamp;
    269         if (timeDelta > maxLatencyNanos) {
    270             __android_log_print(ANDROID_LOG_ERROR, LOG_TAG,
    271                     "  ---- CHECKLATENCY_LATENCYEXCEEDED %ld", timeDelta);
    272             return CHECKLATENCY_LATENCYEXCEEDED;
    273         }
    274     }
    275 
    276     return CHECKLATENCY_SUCCESS;
    277 }
    278 
    279 JNIEXPORT jlong JNICALL Java_android_nativemidi_cts_NativeMidiEchoTest_allocTestContext(
    280         JNIEnv* env, jclass) {
    281     TestContext* context = new TestContext;
    282     if (!context->initN(env)) {
    283         delete context;
    284         context = NULL;
    285     }
    286 
    287     return (jlong)context;
    288 }
    289 
    290 JNIEXPORT void JNICALL Java_android_nativemidi_cts_NativeMidiEchoTest_freeTestContext(
    291         JNIEnv*, jclass, jlong context) {
    292     delete (TestContext*)context;
    293 }
    294 
    295 /*
    296  * Receiving API
    297  */
    298 //static void DumpDataMessage(ReceivedMessageRecord* msg) {
    299 //    char midiDumpBuffer[SIZE_DATABUFFER * 4]; // more than enough
    300 //    memset(midiDumpBuffer, 0, sizeof(midiDumpBuffer));
    301 //    int pos = snprintf(midiDumpBuffer, sizeof(midiDumpBuffer),
    302 //            "%" PRIx64 " ", msg->timestamp);
    303 //    for (uint8_t *b = msg->buffer, *e = b + msg->numDataBytes; b < e; ++b) {
    304 //        pos += snprintf(midiDumpBuffer + pos, sizeof(midiDumpBuffer) - pos,
    305 //                "%02x ", *b);
    306 //    }
    307 //    __android_log_print(ANDROID_LOG_INFO, LOG_TAG, "---- DUMP %s", midiDumpBuffer);
    308 //}
    309 
    310 void readThreadRoutine(TestContext* context) {
    311     int32_t opCode;
    312     uint8_t inDataBuffer[SIZE_DATABUFFER];
    313     size_t numDataBytes;
    314     int64_t timestamp;
    315 
    316     while (context->mReading) {
    317         AMidiOutputPort* outputPort = context->midiOutputPort.load();
    318         if (outputPort != nullptr) {
    319             ssize_t numMessages =
    320                 AMidiOutputPort_receive(outputPort, &opCode,
    321                     inDataBuffer, sizeof(inDataBuffer), &numDataBytes, &timestamp);
    322 
    323             if (numMessages > 0) {
    324                 context->incNumReceives();
    325                 context->incNumBytesReceived(numDataBytes);
    326                 ReceivedMessageRecord receiveRec;
    327                 receiveRec.timeReceived = System_nanoTime();
    328                 receiveRec.numDataBytes = numDataBytes;
    329                 receiveRec.dataBuff.reset(new uint8_t[receiveRec.numDataBytes]);
    330                 memcpy(receiveRec.dataBuff.get(), inDataBuffer, receiveRec.numDataBytes);
    331                 receiveRec.opCode = opCode;
    332                 receiveRec.timestamp = timestamp;
    333                 context->addReceived(std::move(receiveRec));
    334             }
    335         }
    336     }
    337 }
    338 
    339 static media_status_t commonDeviceOpen(JNIEnv *env, jobject midiDeviceObj, AMidiDevice** device) {
    340     media_status_t status = AMidiDevice_fromJava(env, midiDeviceObj, device);
    341     if (status == AMEDIA_OK) {
    342         // __android_log_print(ANDROID_LOG_INFO, LOG_TAG,
    343         //      "---- Obtained device token for obj %p: dev %p", midiDeviceObj, device);
    344     } else {
    345         __android_log_print(ANDROID_LOG_ERROR, LOG_TAG,
    346                 "---- Could not obtain device token for obj %p: status:%d", midiDeviceObj, status);
    347     }
    348 
    349     return status;
    350 }
    351 
    352 JNIEXPORT jint JNICALL Java_android_nativemidi_cts_NativeMidiEchoTest_openNativeMidiDevice(
    353         JNIEnv* env, jobject, jlong ctx, jobject deviceObj) {
    354     TestContext* context = (TestContext*)ctx;
    355     media_status_t status = commonDeviceOpen(env, deviceObj, &context->nativeDevice);
    356     return status;
    357 }
    358 
    359 JNIEXPORT jint JNICALL Java_android_nativemidi_cts_NativeMidiEchoTest_closeNativeMidiDevice(
    360         JNIEnv*, jobject, jlong ctx) {
    361     TestContext* context = (TestContext*)ctx;
    362     media_status_t status = AMidiDevice_release(context->nativeDevice);
    363     return status;
    364 }
    365 
    366 /*
    367  * Sending API
    368  */
    369 JNIEXPORT jint JNICALL Java_android_nativemidi_cts_NativeMidiEchoTest_startWritingMidi(
    370         JNIEnv*, jobject, jlong ctx, jint portNumber) {
    371 
    372     TestContext* context = (TestContext*)ctx;
    373 
    374     AMidiInputPort* inputPort;
    375     media_status_t status = AMidiInputPort_open(context->nativeDevice, portNumber, &inputPort);
    376     if (status == AMEDIA_OK) {
    377         // __android_log_print(ANDROID_LOG_INFO, LOG_TAG,
    378         //      "---- Opened INPUT port %d: token %p", portNumber, inputPort);
    379         context->midiInputPort.store(inputPort);
    380     } else {
    381         __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, "---- Could not open INPUT port %p:%d %d",
    382                 context->nativeDevice, portNumber, status);
    383         return status;
    384     }
    385 
    386     return AMEDIA_OK;
    387 }
    388 
    389 JNIEXPORT jint JNICALL Java_android_nativemidi_cts_NativeMidiEchoTest_stopWritingMidi(
    390         JNIEnv*, jobject, jlong ctx) {
    391 
    392     TestContext* context = (TestContext*)ctx;
    393 
    394     AMidiInputPort* inputPort = context->midiInputPort.exchange(nullptr);
    395     if (inputPort == nullptr) {
    396         return -1;
    397     }
    398 
    399     AMidiInputPort_close(inputPort);
    400 
    401     return 0;
    402 }
    403 
    404 JNIEXPORT jint JNICALL Java_android_nativemidi_cts_NativeMidiEchoTest_writeMidiWithTimestamp(
    405         JNIEnv* env, jobject,
    406         jlong ctx, jbyteArray data, jint offset, jint numBytes, jlong timestamp) {
    407 
    408     TestContext* context = (TestContext*)ctx;
    409     context->incNumSends();
    410     context->incNumBytesSent(numBytes);
    411 
    412     jbyte* bufferPtr = env->GetByteArrayElements(data, NULL);
    413     if (bufferPtr == NULL) {
    414         return -1;
    415     }
    416 
    417     int numWritten =  AMidiInputPort_sendWithTimestamp(
    418             context->midiInputPort, (uint8_t*)bufferPtr + offset, numBytes, timestamp);
    419     if (numWritten > 0) {
    420         // Don't save a send record if we didn't send!
    421         SentMessageRecord sendRec;
    422         sendRec.numDataBytes = numBytes;
    423         sendRec.dataBuff.reset(new uint8_t[sendRec.numDataBytes]);
    424         memcpy(sendRec.dataBuff.get(), (uint8_t*)bufferPtr + offset, numBytes);
    425         sendRec.timestamp = timestamp;
    426         sendRec.timeSent = System_nanoTime();
    427         context->addSent(std::move(sendRec));
    428     }
    429 
    430     env->ReleaseByteArrayElements(data, bufferPtr, JNI_ABORT);
    431 
    432     return numWritten;
    433 }
    434 
    435 JNIEXPORT jint JNICALL Java_android_nativemidi_cts_NativeMidiEchoTest_writeMidi(
    436         JNIEnv* env, jobject j_object, jlong ctx, jbyteArray data, jint offset, jint numBytes) {
    437     return Java_android_nativemidi_cts_NativeMidiEchoTest_writeMidiWithTimestamp(
    438             env, j_object, ctx, data, offset, numBytes, 0L);
    439 }
    440 
    441 JNIEXPORT jint JNICALL Java_android_nativemidi_cts_NativeMidiEchoTest_flushSentMessages(
    442         JNIEnv*, jobject, jlong ctx) {
    443     TestContext* context = (TestContext*)ctx;
    444     return AMidiInputPort_sendFlush(context->midiInputPort);
    445 }
    446 
    447 JNIEXPORT jint JNICALL Java_android_nativemidi_cts_NativeMidiEchoTest_getNumSends(
    448         JNIEnv*, jobject, jlong ctx) {
    449     return ((TestContext*)ctx)->getNumSends();
    450 }
    451 
    452 JNIEXPORT jint JNICALL Java_android_nativemidi_cts_NativeMidiEchoTest_getNumBytesSent(
    453         JNIEnv*, jobject, jlong ctx) {
    454     return ((TestContext*)ctx)->getNumBytesSent();
    455 }
    456 
    457 JNIEXPORT jint JNICALL Java_android_nativemidi_cts_NativeMidiEchoTest_getNumReceives(
    458         JNIEnv*, jobject, jlong ctx) {
    459     return ((TestContext*)ctx)->getNumReceives();
    460 }
    461 
    462 JNIEXPORT jint JNICALL Java_android_nativemidi_cts_NativeMidiEchoTest_getNumBytesReceived(
    463         JNIEnv*, jobject, jlong ctx) {
    464     return ((TestContext*)ctx)->getNumBytesReceived();
    465 }
    466 
    467 JNIEXPORT jint JNICALL Java_android_nativemidi_cts_NativeMidiEchoTest_startReadingMidi(
    468         JNIEnv*, jobject, jlong ctx, jint portNumber) {
    469 
    470     // __android_log_print(ANDROID_LOG_INFO, LOG_TAG, "++++ startReadingMidi()");
    471     TestContext* context = (TestContext*)ctx;
    472 
    473     AMidiOutputPort* outputPort;
    474     media_status_t status = AMidiOutputPort_open(context->nativeDevice, portNumber, &outputPort);
    475     if (status == AMEDIA_OK) {
    476         context->midiOutputPort.store(outputPort);
    477     } else {
    478         __android_log_print(ANDROID_LOG_ERROR, LOG_TAG,
    479                 "---- Could not open OUTPUT port %p : %d %d",
    480                 context->nativeDevice, portNumber, status);
    481         return status;
    482     }
    483 
    484     // Start read thread
    485     context->mReading = true;
    486     context->mReadThread.reset(new std::thread(readThreadRoutine, context));
    487     std::this_thread::yield(); // let the read thread startup.
    488 
    489     return status;
    490 }
    491 
    492 JNIEXPORT jint JNICALL Java_android_nativemidi_cts_NativeMidiEchoTest_stopReadingMidi(
    493         JNIEnv*, jobject, jlong ctx) {
    494 
    495     // __android_log_print(ANDROID_LOG_INFO, LOG_TAG, "++++ stopReadingMidi()");
    496     TestContext* context = (TestContext*)ctx;
    497     context->mReading = false;
    498 
    499     context->mReadThread->join();
    500 
    501     AMidiOutputPort* outputPort = context->midiOutputPort.exchange(nullptr);
    502     if (outputPort == nullptr) {
    503         return -1;
    504     }
    505 
    506     AMidiOutputPort_close(outputPort);
    507 
    508     return 0;
    509 }
    510 
    511 /*
    512  * Messages
    513  */
    514 JNIEXPORT jint JNICALL Java_android_nativemidi_cts_NativeMidiEchoTest_getNumReceivedMessages(
    515         JNIEnv*, jobject, jlong ctx) {
    516     return ((TestContext*)ctx)->getNumReceivedMsgs();
    517 }
    518 
    519 JNIEXPORT jobject JNICALL Java_android_nativemidi_cts_NativeMidiEchoTest_getReceivedMessageAt(
    520         JNIEnv* env, jobject, jlong ctx, jint index) {
    521     return ((TestContext*)ctx)->transferReceiveMsgAt(env, index);
    522 }
    523 
    524 JNIEXPORT jint JNICALL Java_android_nativemidi_cts_NativeMidiEchoTest_matchNativeMessages(
    525         JNIEnv*, jobject, jlong ctx) {
    526     return ((TestContext*)ctx)->compareInsAndOuts();
    527 }
    528 
    529 JNIEXPORT jint JNICALL Java_android_nativemidi_cts_NativeMidiEchoTest_checkNativeLatency(
    530         JNIEnv*, jobject, jlong ctx, jlong maxLatencyNanos) {
    531     return ((TestContext*)ctx)->checkInOutLatency(maxLatencyNanos);
    532 }
    533 
    534 } // extern "C"
    535