/* * Find the return type in the signature, and convert it to a class * object. For primitive types we use a boxed class, for reference types * we do a name lookup. * * On failure, we return NULL with an exception raised. */ ClassObject* dvmGetBoxedReturnType(const Method* meth) { const char* sig = dexProtoGetReturnType(&meth->prototype); switch (*sig) { case 'Z': case 'C': case 'F': case 'D': case 'B': case 'S': case 'I': case 'J': case 'V': return dvmFindPrimitiveClass(*sig); case '[': case 'L': return dvmFindClass(sig, meth->clazz->classLoader); default: { /* should not have passed verification */ char* desc = dexProtoCopyMethodDescriptor(&meth->prototype); ALOGE("Bad return type in signature '%s'", desc); free(desc); dvmThrowInternalError(NULL); return NULL; } } }
static bool performanceTest() { static const int kTableMax = 100; IndirectRefTable irt; IndirectRef manyRefs[kTableMax]; ClassObject* clazz = dvmFindClass("Ljava/lang/Object;", NULL); Object* obj0 = dvmAllocObject(clazz, ALLOC_DONT_TRACK); const u4 cookie = IRT_FIRST_SEGMENT; const int kLoops = 100000; Stopwatch stopwatch; DBUG_MSG("+++ START performance\n"); if (!irt.init(kTableMax, kTableMax, kIndirectKindGlobal)) { return false; } stopwatch.reset(); for (int loop = 0; loop < kLoops; loop++) { for (int i = 0; i < kTableMax; i++) { manyRefs[i] = irt.add(cookie, obj0); } for (int i = 0; i < kTableMax; i++) { irt.remove(cookie, manyRefs[i]); } } DBUG_MSG("Add/remove %d objects FIFO order, %d iterations, %0.3fms / iteration", kTableMax, kLoops, stopwatch.elapsedSeconds() * 1000 / kLoops); stopwatch.reset(); for (int loop = 0; loop < kLoops; loop++) { for (int i = 0; i < kTableMax; i++) { manyRefs[i] = irt.add(cookie, obj0); } for (int i = kTableMax; i-- > 0; ) { irt.remove(cookie, manyRefs[i]); } } DBUG_MSG("Add/remove %d objects LIFO order, %d iterations, %0.3fms / iteration", kTableMax, kLoops, stopwatch.elapsedSeconds() * 1000 / kLoops); for (int i = 0; i < kTableMax; i++) { manyRefs[i] = irt.add(cookie, obj0); } stopwatch.reset(); for (int loop = 0; loop < kLoops; loop++) { for (int i = 0; i < kTableMax; i++) { irt.get(manyRefs[i]); } } DBUG_MSG("Get %d objects, %d iterations, %0.3fms / iteration", kTableMax, kLoops, stopwatch.elapsedSeconds() * 1000 / kLoops); for (int i = kTableMax; i-- > 0; ) { irt.remove(cookie, manyRefs[i]); } irt.destroy(); return true; }
/* * Find a class by name, initializing it if requested. */ ClassObject* dvmFindClassByName(StringObject* nameObj, Object* loader, bool doInit) { ClassObject* clazz = NULL; char* name = NULL; char* descriptor = NULL; if (nameObj == NULL) { dvmThrowException("Ljava/lang/NullPointerException;", NULL); goto bail; } name = dvmCreateCstrFromString(nameObj); /* * We need to validate and convert the name (from x.y.z to x/y/z). This * is especially handy for array types, since we want to avoid * auto-generating bogus array classes. */ if (!validateClassName(name)) { LOGW("dvmFindClassByName rejecting '%s'\n", name); dvmThrowException("Ljava/lang/ClassNotFoundException;", name); goto bail; } descriptor = dvmDotToDescriptor(name); if (descriptor == NULL) { goto bail; } if (doInit) clazz = dvmFindClass(descriptor, loader); else clazz = dvmFindClassNoInit(descriptor, loader); if (clazz == NULL) { LOGVV("FAIL: load %s (%d)\n", descriptor, doInit); Thread* self = dvmThreadSelf(); Object* oldExcep = dvmGetException(self); dvmAddTrackedAlloc(oldExcep, self); /* don't let this be GCed */ dvmClearException(self); dvmThrowChainedException("Ljava/lang/ClassNotFoundException;", name, oldExcep); dvmReleaseTrackedAlloc(oldExcep, self); } else { LOGVV("GOOD: load %s (%d) --> %p ldr=%p\n", descriptor, doInit, clazz, clazz->classLoader); } bail: free(name); free(descriptor); return clazz; }
/* * Broadcast an event to all handlers. */ static void broadcast(int event) { ClassObject* ddmServerClass; Method* bcast; ddmServerClass = dvmFindClass("Lorg/apache/harmony/dalvik/ddmc/DdmServer;", NULL); if (ddmServerClass == NULL) { LOGW("Unable to find org.apache.harmony.dalvik.ddmc.DdmServer\n"); goto bail; } bcast = dvmFindDirectMethodByDescriptor(ddmServerClass, "broadcast", "(I)V"); if (bcast == NULL) { LOGW("Unable to find DdmServer.broadcast\n"); goto bail; } Thread* self = dvmThreadSelf(); if (self->status != THREAD_RUNNING) { LOGE("ERROR: DDM broadcast with thread status=%d\n", self->status); /* try anyway? */ } JValue unused; dvmCallMethod(self, bcast, NULL, &unused, event); if (dvmCheckException(self)) { LOGI("Exception thrown by broadcast(%d)\n", event); dvmLogExceptionStackTrace(); dvmClearException(self); goto bail; } bail: ; }
/* * Basic add/get/delete tests in an unsegmented table. */ static bool basicTest() { static const int kTableMax = 20; IndirectRefTable irt; IndirectRef iref0, iref1, iref2, iref3; IndirectRef manyRefs[kTableMax]; ClassObject* clazz = dvmFindClass("Ljava/lang/Object;", NULL); Object* obj0 = dvmAllocObject(clazz, ALLOC_DONT_TRACK); Object* obj1 = dvmAllocObject(clazz, ALLOC_DONT_TRACK); Object* obj2 = dvmAllocObject(clazz, ALLOC_DONT_TRACK); Object* obj3 = dvmAllocObject(clazz, ALLOC_DONT_TRACK); const u4 cookie = IRT_FIRST_SEGMENT; bool result = false; if (!irt.init(kTableMax/2, kTableMax, kIndirectKindGlobal)) { return false; } iref0 = (IndirectRef) 0x11110; if (irt.remove(cookie, iref0)) { ALOGE("unexpectedly successful removal"); goto bail; } /* * Add three, check, remove in the order in which they were added. */ DBUG_MSG("+++ START fifo\n"); iref0 = irt.add(cookie, obj0); iref1 = irt.add(cookie, obj1); iref2 = irt.add(cookie, obj2); if (iref0 == NULL || iref1 == NULL || iref2 == NULL) { ALOGE("trivial add1 failed"); goto bail; } if (irt.get(iref0) != obj0 || irt.get(iref1) != obj1 || irt.get(iref2) != obj2) { ALOGE("objects don't match expected values %p %p %p vs. %p %p %p", irt.get(iref0), irt.get(iref1), irt.get(iref2), obj0, obj1, obj2); goto bail; } else { DBUG_MSG("+++ obj1=%p --> iref1=%p\n", obj1, iref1); } if (!irt.remove(cookie, iref0) || !irt.remove(cookie, iref1) || !irt.remove(cookie, iref2)) { ALOGE("fifo deletion failed"); goto bail; } /* table should be empty now */ if (irt.capacity() != 0) { ALOGE("fifo del not empty"); goto bail; } /* get invalid entry (off the end of the list) */ if (irt.get(iref0) != kInvalidIndirectRefObject) { ALOGE("stale entry get succeeded unexpectedly"); goto bail; } /* * Add three, remove in the opposite order. */ DBUG_MSG("+++ START lifo\n"); iref0 = irt.add(cookie, obj0); iref1 = irt.add(cookie, obj1); iref2 = irt.add(cookie, obj2); if (iref0 == NULL || iref1 == NULL || iref2 == NULL) { ALOGE("trivial add2 failed"); goto bail; } if (!irt.remove(cookie, iref2) || !irt.remove(cookie, iref1) || !irt.remove(cookie, iref0)) { ALOGE("lifo deletion failed"); goto bail; } /* table should be empty now */ if (irt.capacity() != 0) { ALOGE("lifo del not empty"); goto bail; } /* * Add three, remove middle / middle / bottom / top. (Second attempt * to remove middle should fail.) */ DBUG_MSG("+++ START unorder\n"); iref0 = irt.add(cookie, obj0); iref1 = irt.add(cookie, obj1); iref2 = irt.add(cookie, obj2); if (iref0 == NULL || iref1 == NULL || iref2 == NULL) { ALOGE("trivial add3 failed"); goto bail; } if (irt.capacity() != 3) { ALOGE("expected 3 entries, found %d", irt.capacity()); goto bail; } if (!irt.remove(cookie, iref1) || irt.remove(cookie, iref1)) { ALOGE("unorder deletion1 failed"); goto bail; } /* get invalid entry (from hole) */ if (irt.get(iref1) != kInvalidIndirectRefObject) { ALOGE("hole get succeeded unexpectedly"); goto bail; } if (!irt.remove(cookie, iref2) || !irt.remove(cookie, iref0)) { ALOGE("unorder deletion2 failed"); goto bail; } /* table should be empty now */ if (irt.capacity() != 0) { ALOGE("unorder del not empty"); goto bail; } /* * Add four entries. Remove #1, add new entry, verify that table size * is still 4 (i.e. holes are getting filled). Remove #1 and #3, verify * that we delete one and don't hole-compact the other. */ DBUG_MSG("+++ START hole fill\n"); iref0 = irt.add(cookie, obj0); iref1 = irt.add(cookie, obj1); iref2 = irt.add(cookie, obj2); iref3 = irt.add(cookie, obj3); if (iref0 == NULL || iref1 == NULL || iref2 == NULL || iref3 == NULL) { ALOGE("trivial add4 failed"); goto bail; } if (!irt.remove(cookie, iref1)) { ALOGE("remove 1 of 4 failed"); goto bail; } iref1 = irt.add(cookie, obj1); if (irt.capacity() != 4) { ALOGE("hole not filled"); goto bail; } if (!irt.remove(cookie, iref1) || !irt.remove(cookie, iref3)) { ALOGE("remove 1/3 failed"); goto bail; } if (irt.capacity() != 3) { ALOGE("should be 3 after two deletions"); goto bail; } if (!irt.remove(cookie, iref2) || !irt.remove(cookie, iref0)) { ALOGE("remove 2/0 failed"); goto bail; } if (irt.capacity() != 0) { ALOGE("not empty after split remove"); goto bail; } /* * Add an entry, remove it, add a new entry, and try to use the original * iref. They have the same slot number but are for different objects. * With the extended checks in place, this should fail. */ DBUG_MSG("+++ START switched\n"); iref0 = irt.add(cookie, obj0); irt.remove(cookie, iref0); iref1 = irt.add(cookie, obj1); if (irt.remove(cookie, iref0)) { ALOGE("mismatched del succeeded (%p vs %p)", iref0, iref1); goto bail; } if (!irt.remove(cookie, iref1)) { ALOGE("switched del failed"); goto bail; } if (irt.capacity() != 0) { ALOGE("switching del not empty"); goto bail; } /* * Same as above, but with the same object. A more rigorous checker * (e.g. with slot serialization) will catch this. */ DBUG_MSG("+++ START switched same object\n"); iref0 = irt.add(cookie, obj0); irt.remove(cookie, iref0); iref1 = irt.add(cookie, obj0); if (iref0 != iref1) { /* try 0, should not work */ if (irt.remove(cookie, iref0)) { ALOGE("temporal del succeeded (%p vs %p)", iref0, iref1); goto bail; } } if (!irt.remove(cookie, iref1)) { ALOGE("temporal cleanup failed"); goto bail; } if (irt.capacity() != 0) { ALOGE("temporal del not empty"); goto bail; } DBUG_MSG("+++ START null lookup\n"); if (irt.get(NULL) != kInvalidIndirectRefObject) { ALOGE("null lookup succeeded"); goto bail; } DBUG_MSG("+++ START stale lookup\n"); iref0 = irt.add(cookie, obj0); irt.remove(cookie, iref0); if (irt.get(iref0) != kInvalidIndirectRefObject) { ALOGE("stale lookup succeeded"); goto bail; } /* * Test table overflow. */ DBUG_MSG("+++ START overflow\n"); int i; for (i = 0; i < kTableMax; i++) { manyRefs[i] = irt.add(cookie, obj0); if (manyRefs[i] == NULL) { ALOGE("Failed adding %d of %d", i, kTableMax); goto bail; } } if (irt.add(cookie, obj0) != NULL) { ALOGE("Table overflow succeeded"); goto bail; } if (irt.capacity() != (size_t)kTableMax) { ALOGE("Expected %d entries, found %d", kTableMax, irt.capacity()); goto bail; } irt.dump("table with 20 entries, all filled"); for (i = 0; i < kTableMax-1; i++) { if (!irt.remove(cookie, manyRefs[i])) { ALOGE("multi-remove failed at %d", i); goto bail; } } irt.dump("table with 20 entries, 19 of them holes"); /* because of removal order, should have 20 entries, 19 of them holes */ if (irt.capacity() != (size_t)kTableMax) { ALOGE("Expected %d entries (with holes), found %d", kTableMax, irt.capacity()); goto bail; } if (!irt.remove(cookie, manyRefs[kTableMax-1])) { ALOGE("multi-remove final failed"); goto bail; } if (irt.capacity() != 0) { ALOGE("multi-del not empty"); goto bail; } /* Done */ DBUG_MSG("+++ basic test complete\n"); result = true; bail: irt.destroy(); return result; }
/* * Test operations on a segmented table. */ static bool segmentTest(void) { static const int kTableMax = 20; IndirectRefTable irt; IndirectRef iref0, iref1, iref2, iref3; ClassObject* clazz = dvmFindClass("Ljava/lang/Object;", NULL); Object* obj0 = dvmAllocObject(clazz, ALLOC_DONT_TRACK); Object* obj1 = dvmAllocObject(clazz, ALLOC_DONT_TRACK); Object* obj2 = dvmAllocObject(clazz, ALLOC_DONT_TRACK); Object* obj3 = dvmAllocObject(clazz, ALLOC_DONT_TRACK); u4 cookie; u4 segmentState[4]; bool result = false; if (!dvmInitIndirectRefTable(&irt, kTableMax, kTableMax, kIndirectKindLocal)) { return false; } cookie = segmentState[0] = IRT_FIRST_SEGMENT; DBUG_MSG("+++ objs %p %p %p %p\n", obj0, obj1, obj2, obj3); /* * Push two, create new segment, push two more, try to get all four, * try to delete all 4. All four should be accessible, but only the * last two should be deletable. */ DBUG_MSG("+++ START basic segment\n"); iref0 = dvmAddToIndirectRefTable(&irt, cookie, obj0); iref1 = dvmAddToIndirectRefTable(&irt, cookie, obj1); cookie = segmentState[1] = dvmPushIndirectRefTableSegment(&irt); DBUG_MSG("+++ pushed, cookie is 0x%08x\n", cookie); iref2 = dvmAddToIndirectRefTable(&irt, cookie, obj2); iref3 = dvmAddToIndirectRefTable(&irt, cookie, obj3); if (dvmRemoveFromIndirectRefTable(&irt, cookie, iref0) || dvmRemoveFromIndirectRefTable(&irt, cookie, iref1)) { LOGE("removed values from earlier segment\n"); goto bail; } if (!dvmRemoveFromIndirectRefTable(&irt, cookie, iref2) || !dvmRemoveFromIndirectRefTable(&irt, cookie, iref3)) { LOGE("unable to remove values from current segment\n"); goto bail; } if (dvmIndirectRefTableEntries(&irt) != 2) { LOGE("wrong total entries\n"); goto bail; } dvmPopIndirectRefTableSegment(&irt, segmentState[1]); cookie = segmentState[0]; if (!dvmRemoveFromIndirectRefTable(&irt, cookie, iref0) || !dvmRemoveFromIndirectRefTable(&irt, cookie, iref1)) { LOGE("unable to remove values from first segment\n"); goto bail; } if (dvmIndirectRefTableEntries(&irt) != 0) { LOGE("basic push/pop not empty\n"); goto bail; } /* * Push two, delete first, segment, push two more, pop segment, verify * the last two are no longer present and hole count is right. The * adds after the segment pop should not be filling in the hole. */ DBUG_MSG("+++ START segment pop\n"); iref0 = dvmAddToIndirectRefTable(&irt, cookie, obj0); iref1 = dvmAddToIndirectRefTable(&irt, cookie, obj1); dvmRemoveFromIndirectRefTable(&irt, cookie, iref0); cookie = segmentState[1] = dvmPushIndirectRefTableSegment(&irt); iref2 = dvmAddToIndirectRefTable(&irt, cookie, obj2); iref3 = dvmAddToIndirectRefTable(&irt, cookie, obj3); dvmPopIndirectRefTableSegment(&irt, segmentState[1]); cookie = segmentState[0]; if (dvmIndirectRefTableEntries(&irt) != 2) { LOGE("wrong total entries after pop\n"); goto bail; } dvmRemoveFromIndirectRefTable(&irt, cookie, iref1); if (dvmIndirectRefTableEntries(&irt) != 0) { LOGE("not back to zero after pop + del\n"); goto bail; } /* * Multiple segments, some empty. */ DBUG_MSG("+++ START multiseg\n"); iref0 = dvmAppendToIndirectRefTable(&irt, cookie, obj0); iref1 = dvmAppendToIndirectRefTable(&irt, cookie, obj1); cookie = segmentState[1] = dvmPushIndirectRefTableSegment(&irt); cookie = segmentState[2] = dvmPushIndirectRefTableSegment(&irt); iref3 = dvmAppendToIndirectRefTable(&irt, cookie, obj3); iref2 = dvmAppendToIndirectRefTable(&irt, cookie, obj2); dvmRemoveFromIndirectRefTable(&irt, cookie, iref3); cookie = segmentState[3] = dvmPushIndirectRefTableSegment(&irt); iref3 = dvmAppendToIndirectRefTable(&irt, cookie, obj3); if (dvmGetFromIndirectRefTable(&irt, iref0) != obj0 || dvmGetFromIndirectRefTable(&irt, iref1) != obj1 || dvmGetFromIndirectRefTable(&irt, iref2) != obj2 || dvmGetFromIndirectRefTable(&irt, iref3) != obj3) { LOGE("Unable to retrieve all multiseg objects\n"); goto bail; } dvmDumpIndirectRefTable(&irt, "test"); //int i; //for (i = 0; i < sizeof(segmentState) / sizeof(segmentState[0]); i++) { // DBUG_MSG("+++ segment %d = 0x%08x\n", i, segmentState[i]); //} dvmRemoveFromIndirectRefTable(&irt, cookie, iref3); if (dvmRemoveFromIndirectRefTable(&irt, cookie, iref2)) { LOGE("multiseg del2 worked\n"); goto bail; } dvmPopIndirectRefTableSegment(&irt, segmentState[3]); cookie = segmentState[2]; if (!dvmRemoveFromIndirectRefTable(&irt, cookie, iref2)) { LOGE("multiseg del2b failed (cookie=0x%08x ref=%p)\n", cookie, iref2); goto bail; } iref2 = dvmAddToIndirectRefTable(&irt, cookie, obj2); /* pop two off at once */ dvmPopIndirectRefTableSegment(&irt, segmentState[1]); cookie = segmentState[0]; if (dvmIndirectRefTableEntries(&irt) != 2) { LOGE("Unexpected entry count in multiseg\n"); goto bail; } dvmRemoveFromIndirectRefTable(&irt, cookie, iref0); dvmRemoveFromIndirectRefTable(&irt, cookie, iref1); if (dvmIndirectRefTableEntries(&irt) != 0) { LOGE("Unexpected entry count at multiseg end\n"); goto bail; } DBUG_MSG("+++ segment test complete\n"); result = true; bail: dvmClearIndirectRefTable(&irt); return result; }
/* * "buf" contains a full JDWP packet, possibly with multiple chunks. We * need to process each, accumulate the replies, and ship the whole thing * back. * * Returns "true" if we have a reply. The reply buffer is newly allocated, * and includes the chunk type/length, followed by the data. * * TODO: we currently assume that the request and reply include a single * chunk. If this becomes inconvenient we will need to adapt. */ bool dvmDdmHandlePacket(const u1* buf, int dataLen, u1** pReplyBuf, int* pReplyLen) { Thread* self = dvmThreadSelf(); const int kChunkHdrLen = 8; ArrayObject* dataArray = NULL; bool result = false; assert(dataLen >= 0); /* * Prep DdmServer. We could throw this in gDvm. */ ClassObject* ddmServerClass; Method* dispatch; ddmServerClass = dvmFindClass("Lorg/apache/harmony/dalvik/ddmc/DdmServer;", NULL); if (ddmServerClass == NULL) { LOGW("Unable to find org.apache.harmony.dalvik.ddmc.DdmServer\n"); goto bail; } dispatch = dvmFindDirectMethodByDescriptor(ddmServerClass, "dispatch", "(I[BII)Lorg/apache/harmony/dalvik/ddmc/Chunk;"); if (dispatch == NULL) { LOGW("Unable to find DdmServer.dispatch\n"); goto bail; } /* * Prep Chunk. */ int chunkTypeOff, chunkDataOff, chunkOffsetOff, chunkLengthOff; ClassObject* chunkClass; chunkClass = dvmFindClass("Lorg/apache/harmony/dalvik/ddmc/Chunk;", NULL); if (chunkClass == NULL) { LOGW("Unable to find org.apache.harmony.dalvik.ddmc.Chunk\n"); goto bail; } chunkTypeOff = dvmFindFieldOffset(chunkClass, "type", "I"); chunkDataOff = dvmFindFieldOffset(chunkClass, "data", "[B"); chunkOffsetOff = dvmFindFieldOffset(chunkClass, "offset", "I"); chunkLengthOff = dvmFindFieldOffset(chunkClass, "length", "I"); if (chunkTypeOff < 0 || chunkDataOff < 0 || chunkOffsetOff < 0 || chunkLengthOff < 0) { LOGW("Unable to find all chunk fields\n"); goto bail; } /* * The chunk handlers are written in the Java programming language, so * we need to convert the buffer to a byte array. */ dataArray = dvmAllocPrimitiveArray('B', dataLen, ALLOC_DEFAULT); if (dataArray == NULL) { LOGW("array alloc failed (%d)\n", dataLen); dvmClearException(self); goto bail; } memcpy(dataArray->contents, buf, dataLen); /* * Run through and find all chunks. [Currently just find the first.] */ unsigned int offset, length, type; type = get4BE((u1*)dataArray->contents + 0); length = get4BE((u1*)dataArray->contents + 4); offset = kChunkHdrLen; if (offset+length > (unsigned int) dataLen) { LOGW("WARNING: bad chunk found (len=%u pktLen=%d)\n", length, dataLen); goto bail; } /* * Call the handler. */ JValue callRes; dvmCallMethod(self, dispatch, NULL, &callRes, type, dataArray, offset, length); if (dvmCheckException(self)) { LOGI("Exception thrown by dispatcher for 0x%08x\n", type); dvmLogExceptionStackTrace(); dvmClearException(self); goto bail; } Object* chunk; ArrayObject* replyData; chunk = (Object*) callRes.l; if (chunk == NULL) goto bail; /* * Pull the pieces out of the chunk. We copy the results into a * newly-allocated buffer that the caller can free. We don't want to * continue using the Chunk object because nothing has a reference to it. * (If we do an alloc in here, we need to dvmAddTrackedAlloc it.) * * We could avoid this by returning type/data/offset/length and having * the caller be aware of the object lifetime issues, but that * integrates the JDWP code more tightly into the VM, and doesn't work * if we have responses for multiple chunks. * * So we're pretty much stuck with copying data around multiple times. */ type = dvmGetFieldInt(chunk, chunkTypeOff); replyData = (ArrayObject*) dvmGetFieldObject(chunk, chunkDataOff); offset = dvmGetFieldInt(chunk, chunkOffsetOff); length = dvmGetFieldInt(chunk, chunkLengthOff); LOGV("DDM reply: type=0x%08x data=%p offset=%d length=%d\n", type, replyData, offset, length); if (length == 0 || replyData == NULL) goto bail; if (offset + length > replyData->length) { LOGW("WARNING: chunk off=%d len=%d exceeds reply array len %d\n", offset, length, replyData->length); goto bail; } u1* reply; reply = (u1*) malloc(length + kChunkHdrLen); if (reply == NULL) { LOGW("malloc %d failed\n", length+kChunkHdrLen); goto bail; } set4BE(reply + 0, type); set4BE(reply + 4, length); memcpy(reply+kChunkHdrLen, (const u1*)replyData->contents + offset, length); *pReplyBuf = reply; *pReplyLen = length + kChunkHdrLen; result = true; LOGV("dvmHandleDdm returning type=%.4s buf=%p len=%d\n", (char*) reply, reply, length); bail: dvmReleaseTrackedAlloc((Object*) dataArray, NULL); return result; }