summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--runtime/indirect_reference_table-inl.h51
-rw-r--r--runtime/indirect_reference_table.h9
-rw-r--r--runtime/indirect_reference_table_test.cc26
-rw-r--r--runtime/jni/check_jni.cc81
-rw-r--r--runtime/jni/java_vm_ext.h4
-rw-r--r--runtime/jni/jni_env_ext.h3
-rw-r--r--runtime/jni/jni_internal_test.cc43
-rw-r--r--runtime/thread.cc22
8 files changed, 134 insertions, 105 deletions
diff --git a/runtime/indirect_reference_table-inl.h b/runtime/indirect_reference_table-inl.h
index 2128f8cde8..fb2dc1f879 100644
--- a/runtime/indirect_reference_table-inl.h
+++ b/runtime/indirect_reference_table-inl.h
@@ -33,36 +33,28 @@ class Object;
// Verifies that the indirect table lookup is valid.
// Returns "false" if something looks bad.
-inline bool IndirectReferenceTable::GetChecked(IndirectRef iref) const {
- if (UNLIKELY(iref == nullptr)) {
- LOG(WARNING) << "Attempt to look up nullptr " << kind_;
- return false;
- }
- if (UNLIKELY(GetIndirectRefKind(iref) == kHandleScopeOrInvalid)) {
- AbortIfNoCheckJNI(android::base::StringPrintf("JNI ERROR (app bug): invalid %s %p",
- GetIndirectRefKindString(kind_),
- iref));
- return false;
- }
+inline bool IndirectReferenceTable::IsValidReference(IndirectRef iref,
+ /*out*/std::string* error_msg) const {
+ DCHECK(iref != nullptr);
+ DCHECK_EQ(GetIndirectRefKind(iref), kind_);
const uint32_t top_index = segment_state_.top_index;
uint32_t idx = ExtractIndex(iref);
if (UNLIKELY(idx >= top_index)) {
- std::string msg = android::base::StringPrintf(
- "JNI ERROR (app bug): accessed stale %s %p (index %d in a table of size %d)",
- GetIndirectRefKindString(kind_),
- iref,
- idx,
- top_index);
- AbortIfNoCheckJNI(msg);
+ *error_msg = android::base::StringPrintf("deleted reference at index %u in a table of size %u",
+ idx,
+ top_index);
return false;
}
if (UNLIKELY(table_[idx].GetReference()->IsNull())) {
- AbortIfNoCheckJNI(android::base::StringPrintf("JNI ERROR (app bug): accessed deleted %s %p",
- GetIndirectRefKindString(kind_),
- iref));
+ *error_msg = android::base::StringPrintf("deleted reference at index %u", idx);
return false;
}
- if (UNLIKELY(!CheckEntry("use", iref, idx))) {
+ uint32_t iref_serial = DecodeSerial(reinterpret_cast<uintptr_t>(iref));
+ uint32_t entry_serial = table_[idx].GetSerial();
+ if (UNLIKELY(iref_serial != entry_serial)) {
+ *error_msg = android::base::StringPrintf("stale reference with serial number %u v. current %u",
+ iref_serial,
+ entry_serial);
return false;
}
return true;
@@ -88,21 +80,22 @@ inline bool IndirectReferenceTable::CheckEntry(const char* what,
template<ReadBarrierOption kReadBarrierOption>
inline ObjPtr<mirror::Object> IndirectReferenceTable::Get(IndirectRef iref) const {
- if (!GetChecked(iref)) {
- return nullptr;
- }
+ DCHECK_EQ(GetIndirectRefKind(iref), kind_);
uint32_t idx = ExtractIndex(iref);
+ DCHECK_LT(idx, segment_state_.top_index);
+ DCHECK_EQ(DecodeSerial(reinterpret_cast<uintptr_t>(iref)), table_[idx].GetSerial());
+ DCHECK(!table_[idx].GetReference()->IsNull());
ObjPtr<mirror::Object> obj = table_[idx].GetReference()->Read<kReadBarrierOption>();
VerifyObject(obj);
return obj;
}
inline void IndirectReferenceTable::Update(IndirectRef iref, ObjPtr<mirror::Object> obj) {
- if (!GetChecked(iref)) {
- LOG(WARNING) << "IndirectReferenceTable Update failed to find reference " << iref;
- return;
- }
+ DCHECK_EQ(GetIndirectRefKind(iref), kind_);
uint32_t idx = ExtractIndex(iref);
+ DCHECK_LT(idx, segment_state_.top_index);
+ DCHECK_EQ(DecodeSerial(reinterpret_cast<uintptr_t>(iref)), table_[idx].GetSerial());
+ DCHECK(!table_[idx].GetReference()->IsNull());
table_[idx].SetReference(obj);
}
diff --git a/runtime/indirect_reference_table.h b/runtime/indirect_reference_table.h
index 86b92cff2d..f877ce8ba7 100644
--- a/runtime/indirect_reference_table.h
+++ b/runtime/indirect_reference_table.h
@@ -285,6 +285,10 @@ class IndirectReferenceTable {
REQUIRES_SHARED(Locks::mutator_lock_)
REQUIRES(!Locks::alloc_tracker_lock_);
+ IndirectRefKind GetKind() const {
+ return kind_;
+ }
+
// Return the #of entries in the entire table. This includes holes, and
// so may be larger than the actual number of "live" entries.
size_t Capacity() const {
@@ -331,6 +335,10 @@ class IndirectReferenceTable {
return DecodeIndirectRefKind(reinterpret_cast<uintptr_t>(iref));
}
+ /* Reference validation for CheckJNI. */
+ bool IsValidReference(IndirectRef, /*out*/std::string* error_msg) const
+ REQUIRES_SHARED(Locks::mutator_lock_);
+
private:
static constexpr size_t kSerialBits = MinimumBitsToStore(kIRTPrevCount);
static constexpr uint32_t kShiftedSerialMask = (1u << kSerialBits) - 1;
@@ -391,7 +399,6 @@ class IndirectReferenceTable {
static void AbortIfNoCheckJNI(const std::string& msg);
/* extra debugging checks */
- bool GetChecked(IndirectRef) const REQUIRES_SHARED(Locks::mutator_lock_);
bool CheckEntry(const char*, IndirectRef, uint32_t) const;
/// semi-public - read/write by jni down calls.
diff --git a/runtime/indirect_reference_table_test.cc b/runtime/indirect_reference_table_test.cc
index c5ae4c63f7..5da7a30ece 100644
--- a/runtime/indirect_reference_table_test.cc
+++ b/runtime/indirect_reference_table_test.cc
@@ -106,8 +106,9 @@ TEST_F(IndirectReferenceTableTest, BasicTest) {
// Table should be empty now.
EXPECT_EQ(0U, irt.Capacity());
- // Get invalid entry (off the end of the list).
- EXPECT_TRUE(irt.Get(iref0) == nullptr);
+ // Check that the entry off the end of the list is not valid.
+ // (CheckJNI shall abort for such entries.)
+ EXPECT_FALSE(irt.IsValidReference(iref0, &error_msg));
// Add three, remove in the opposite order.
iref0 = irt.Add(cookie, obj0.Get(), &error_msg);
@@ -145,8 +146,8 @@ TEST_F(IndirectReferenceTableTest, BasicTest) {
ASSERT_FALSE(irt.Remove(cookie, iref1));
CheckDump(&irt, 2, 2);
- // Get invalid entry (from hole).
- EXPECT_TRUE(irt.Get(iref1) == nullptr);
+ // Check that the reference to the hole is not valid.
+ EXPECT_FALSE(irt.IsValidReference(iref1, &error_msg));
ASSERT_TRUE(irt.Remove(cookie, iref2));
CheckDump(&irt, 1, 1);
@@ -227,15 +228,12 @@ TEST_F(IndirectReferenceTableTest, BasicTest) {
ASSERT_EQ(0U, irt.Capacity()) << "temporal del not empty";
CheckDump(&irt, 0, 0);
- // null isn't a valid iref.
- ASSERT_TRUE(irt.Get(nullptr) == nullptr);
-
- // Stale lookup.
+ // Stale reference is not valid.
iref0 = irt.Add(cookie, obj0.Get(), &error_msg);
EXPECT_TRUE(iref0 != nullptr);
CheckDump(&irt, 1, 1);
ASSERT_TRUE(irt.Remove(cookie, iref0));
- EXPECT_TRUE(irt.Get(iref0) == nullptr) << "stale lookup succeeded";
+ EXPECT_FALSE(irt.IsValidReference(iref0, &error_msg)) << "stale lookup succeeded";
CheckDump(&irt, 0, 0);
// Test table resizing.
@@ -322,7 +320,7 @@ TEST_F(IndirectReferenceTableTest, Holes) {
// Must not have filled the previous hole.
EXPECT_EQ(irt.Capacity(), 4u);
- EXPECT_TRUE(irt.Get(iref1) == nullptr);
+ EXPECT_FALSE(irt.IsValidReference(iref1, &error_msg));
CheckDump(&irt, 3, 3);
UNUSED(iref0, iref1, iref2, iref3);
@@ -357,7 +355,7 @@ TEST_F(IndirectReferenceTableTest, Holes) {
IndirectRef iref4 = irt.Add(cookie1, obj4.Get(), &error_msg);
EXPECT_EQ(irt.Capacity(), 2u);
- EXPECT_TRUE(irt.Get(iref2) == nullptr);
+ EXPECT_FALSE(irt.IsValidReference(iref2, &error_msg));
CheckDump(&irt, 2, 2);
UNUSED(iref0, iref1, iref2, iref3, iref4);
@@ -397,7 +395,7 @@ TEST_F(IndirectReferenceTableTest, Holes) {
IndirectRef iref4 = irt.Add(cookie1, obj4.Get(), &error_msg);
EXPECT_EQ(irt.Capacity(), 3u);
- EXPECT_TRUE(irt.Get(iref1) == nullptr);
+ EXPECT_FALSE(irt.IsValidReference(iref1, &error_msg));
CheckDump(&irt, 3, 3);
UNUSED(iref0, iref1, iref2, iref3, iref4);
@@ -439,7 +437,7 @@ TEST_F(IndirectReferenceTableTest, Holes) {
IndirectRef iref5 = irt.Add(cookie1, obj4.Get(), &error_msg);
EXPECT_EQ(irt.Capacity(), 2u);
- EXPECT_TRUE(irt.Get(iref3) == nullptr);
+ EXPECT_FALSE(irt.IsValidReference(iref3, &error_msg));
CheckDump(&irt, 2, 2);
UNUSED(iref0, iref1, iref2, iref3, iref4, iref5);
@@ -479,7 +477,7 @@ TEST_F(IndirectReferenceTableTest, Holes) {
IndirectRef iref4 = irt.Add(cookie1, obj3.Get(), &error_msg);
EXPECT_EQ(irt.Capacity(), 2u);
- EXPECT_TRUE(irt.Get(iref3) == nullptr);
+ EXPECT_FALSE(irt.IsValidReference(iref3, &error_msg));
CheckDump(&irt, 2, 2);
UNUSED(iref0, iref1, iref2, iref3, iref4);
diff --git a/runtime/jni/check_jni.cc b/runtime/jni/check_jni.cc
index 1626357709..42e46e9435 100644
--- a/runtime/jni/check_jni.cc
+++ b/runtime/jni/check_jni.cc
@@ -35,6 +35,7 @@
#include "dex/descriptors_names.h"
#include "dex/dex_file-inl.h"
#include "gc/space/space.h"
+#include "indirect_reference_table-inl.h"
#include "java_vm_ext.h"
#include "jni_internal.h"
#include "mirror/class-inl.h"
@@ -50,6 +51,20 @@
#include "well_known_classes.h"
namespace art {
+
+// This helper cannot be in the anonymous namespace because it needs to be
+// declared as a friend by JniVmExt and JniEnvExt.
+inline IndirectReferenceTable* GetIndirectReferenceTable(ScopedObjectAccess& soa,
+ IndirectRefKind kind) {
+ DCHECK_NE(kind, kHandleScopeOrInvalid);
+ JNIEnvExt* env = soa.Env();
+ IndirectReferenceTable* irt =
+ (kind == kLocal) ? &env->locals_
+ : ((kind == kGlobal) ? &env->vm_->globals_ : &env->vm_->weak_globals_);
+ DCHECK_EQ(irt->GetKind(), kind);
+ return irt;
+}
+
namespace {
using android::base::StringAppendF;
@@ -842,26 +857,59 @@ class ScopedCheck {
}
}
- ObjPtr<mirror::Object> obj = soa.Decode<mirror::Object>(java_object);
- if (obj == nullptr) {
- // Either java_object is invalid or is a cleared weak.
- IndirectRef ref = reinterpret_cast<IndirectRef>(java_object);
- bool okay;
- if (IndirectReferenceTable::GetIndirectRefKind(ref) != kWeakGlobal) {
+ ObjPtr<mirror::Object> obj = nullptr;
+ IndirectRef ref = reinterpret_cast<IndirectRef>(java_object);
+ IndirectRefKind ref_kind = IndirectReferenceTable::GetIndirectRefKind(ref);
+ bool expect_null = false;
+ bool okay = true;
+ std::string error_msg;
+ if (ref_kind == kHandleScopeOrInvalid) {
+ if (!soa.Self()->HandleScopeContains(java_object)) {
okay = false;
+ error_msg = "use of invalid jobject";
} else {
- obj = soa.Vm()->DecodeWeakGlobal(soa.Self(), ref);
- okay = Runtime::Current()->IsClearedJniWeakGlobal(obj);
- }
- if (!okay) {
- AbortF("%s is an invalid %s: %p (%p)",
- what,
- GetIndirectRefKindString(IndirectReferenceTable::GetIndirectRefKind(java_object)),
- java_object,
- obj.Ptr());
- return false;
+ obj = soa.Decode<mirror::Object>(java_object);
+ }
+ } else {
+ IndirectReferenceTable* irt = GetIndirectReferenceTable(soa, ref_kind);
+ okay = irt->IsValidReference(java_object, &error_msg);
+ DCHECK_EQ(okay, error_msg.empty());
+ if (okay) {
+ // Note: The `IsValidReference()` checks for null but we do not prevent races,
+ // so the null check below can still fail. Even if it succeeds, another thread
+ // could delete the global or weak global before it's used by JNI.
+ if (ref_kind == kLocal) {
+ // Local references do not need a read barrier.
+ obj = irt->Get<kWithoutReadBarrier>(ref);
+ } else if (ref_kind == kGlobal) {
+ obj = soa.Env()->GetVm()->DecodeGlobal(ref);
+ } else {
+ obj = soa.Env()->GetVm()->DecodeWeakGlobal(soa.Self(), ref);
+ if (Runtime::Current()->IsClearedJniWeakGlobal(obj)) {
+ obj = nullptr;
+ expect_null = true;
+ }
+ }
}
}
+ if (okay) {
+ if (!expect_null && obj == nullptr) {
+ okay = false;
+ error_msg = "deleted reference";
+ }
+ if (expect_null && !null_ok) {
+ okay = false;
+ error_msg = "cleared weak reference";
+ }
+ }
+ if (!okay) {
+ AbortF("JNI ERROR (app bug): %s is an invalid %s: %p (%s)",
+ what,
+ ToStr<IndirectRefKind>(ref_kind).c_str(),
+ java_object,
+ error_msg.c_str());
+ return false;
+ }
if (!Runtime::Current()->GetHeap()->IsValidObjectAddress(obj.Ptr())) {
Runtime::Current()->GetHeap()->DumpSpaces(LOG_STREAM(ERROR));
@@ -873,7 +921,6 @@ class ScopedCheck {
return false;
}
- bool okay = true;
switch (kind) {
case kClass:
okay = obj->IsClass();
diff --git a/runtime/jni/java_vm_ext.h b/runtime/jni/java_vm_ext.h
index 6f7e546de5..015f85cba9 100644
--- a/runtime/jni/java_vm_ext.h
+++ b/runtime/jni/java_vm_ext.h
@@ -37,6 +37,7 @@ class Libraries;
class ParsedOptions;
class Runtime;
struct RuntimeArgumentMap;
+class ScopedObjectAccess;
class JavaVMExt;
// Hook definition for runtime plugins.
@@ -263,6 +264,9 @@ class JavaVMExt : public JavaVM {
std::atomic<bool> allocation_tracking_enabled_;
std::atomic<bool> old_allocation_tracking_state_;
+ friend IndirectReferenceTable* GetIndirectReferenceTable(ScopedObjectAccess& soa,
+ IndirectRefKind kind);
+
DISALLOW_COPY_AND_ASSIGN(JavaVMExt);
};
diff --git a/runtime/jni/jni_env_ext.h b/runtime/jni/jni_env_ext.h
index 5d4304d508..2fae8d286d 100644
--- a/runtime/jni/jni_env_ext.h
+++ b/runtime/jni/jni_env_ext.h
@@ -30,6 +30,7 @@ namespace art {
class ArtMethod;
class ArtField;
class JavaVMExt;
+class ScopedObjectAccess;
class ScopedObjectAccessAlreadyRunnable;
namespace mirror {
@@ -212,6 +213,8 @@ class JNIEnvExt : public JNIEnv {
template<bool kEnableIndexIds> friend class JNI;
friend class ScopedJniEnvLocalRefState;
friend class Thread;
+ friend IndirectReferenceTable* GetIndirectReferenceTable(ScopedObjectAccess& soa,
+ IndirectRefKind kind);
friend void ThreadResetFunctionTable(Thread* thread, void* arg);
ART_FRIEND_TEST(JniInternalTest, JNIEnvExtOffsets);
};
diff --git a/runtime/jni/jni_internal_test.cc b/runtime/jni/jni_internal_test.cc
index a4cfd3a82f..2793857203 100644
--- a/runtime/jni/jni_internal_test.cc
+++ b/runtime/jni/jni_internal_test.cc
@@ -1996,17 +1996,12 @@ TEST_F(JniInternalTest, DeleteLocalRef) {
ASSERT_NE(s, nullptr);
env_->DeleteLocalRef(s);
- // Currently, deleting an already-deleted reference is just a CheckJNI warning.
+ // Currently, deleting an already-deleted reference is just a CheckJNI abort.
{
- bool old_check_jni = vm_->SetCheckJniEnabled(false);
- {
- CheckJniAbortCatcher check_jni_abort_catcher;
- env_->DeleteLocalRef(s);
- }
+ bool old_check_jni = vm_->SetCheckJniEnabled(true);
CheckJniAbortCatcher check_jni_abort_catcher;
- EXPECT_FALSE(vm_->SetCheckJniEnabled(true));
env_->DeleteLocalRef(s);
- std::string expected(StringPrintf("use of deleted local reference %p", s));
+ std::string expected = StringPrintf("jobject is an invalid local reference: %p", s);
check_jni_abort_catcher.Check(expected.c_str());
EXPECT_TRUE(vm_->SetCheckJniEnabled(old_check_jni));
}
@@ -2061,7 +2056,7 @@ TEST_F(JniInternalTest, PushLocalFrame_PopLocalFrame) {
{
CheckJniAbortCatcher check_jni_abort_catcher;
EXPECT_EQ(JNIInvalidRefType, env_->GetObjectRefType(inner1));
- check_jni_abort_catcher.Check("use of deleted local reference");
+ check_jni_abort_catcher.Check("jobject is an invalid local reference");
}
// Our local reference for the survivor is invalid because the survivor
@@ -2069,7 +2064,7 @@ TEST_F(JniInternalTest, PushLocalFrame_PopLocalFrame) {
{
CheckJniAbortCatcher check_jni_abort_catcher;
EXPECT_EQ(JNIInvalidRefType, env_->GetObjectRefType(inner2));
- check_jni_abort_catcher.Check("use of deleted local reference");
+ check_jni_abort_catcher.Check("jobject is an invalid local reference");
}
EXPECT_EQ(env_->PopLocalFrame(nullptr), nullptr);
@@ -2077,11 +2072,11 @@ TEST_F(JniInternalTest, PushLocalFrame_PopLocalFrame) {
EXPECT_EQ(JNILocalRefType, env_->GetObjectRefType(original));
CheckJniAbortCatcher check_jni_abort_catcher;
EXPECT_EQ(JNIInvalidRefType, env_->GetObjectRefType(outer));
- check_jni_abort_catcher.Check("use of deleted local reference");
+ check_jni_abort_catcher.Check("jobject is an invalid local reference");
EXPECT_EQ(JNIInvalidRefType, env_->GetObjectRefType(inner1));
- check_jni_abort_catcher.Check("use of deleted local reference");
+ check_jni_abort_catcher.Check("jobject is an invalid local reference");
EXPECT_EQ(JNIInvalidRefType, env_->GetObjectRefType(inner2));
- check_jni_abort_catcher.Check("use of deleted local reference");
+ check_jni_abort_catcher.Check("jobject is an invalid local reference");
}
TEST_F(JniInternalTest, PushLocalFrame_LimitAndOverflow) {
@@ -2135,17 +2130,12 @@ TEST_F(JniInternalTest, DeleteGlobalRef) {
ASSERT_NE(o, nullptr);
env_->DeleteGlobalRef(o);
- // Currently, deleting an already-deleted reference is just a CheckJNI warning.
+ // Currently, deleting an already-deleted reference is just a CheckJNI abort.
{
- bool old_check_jni = vm_->SetCheckJniEnabled(false);
- {
- CheckJniAbortCatcher check_jni_abort_catcher;
- env_->DeleteGlobalRef(o);
- }
+ bool old_check_jni = vm_->SetCheckJniEnabled(true);
CheckJniAbortCatcher check_jni_abort_catcher;
- EXPECT_FALSE(vm_->SetCheckJniEnabled(true));
env_->DeleteGlobalRef(o);
- std::string expected(StringPrintf("use of deleted global reference %p", o));
+ std::string expected = StringPrintf("jobject is an invalid global reference: %p", o);
check_jni_abort_catcher.Check(expected.c_str());
EXPECT_TRUE(vm_->SetCheckJniEnabled(old_check_jni));
}
@@ -2188,17 +2178,12 @@ TEST_F(JniInternalTest, DeleteWeakGlobalRef) {
ASSERT_NE(o, nullptr);
env_->DeleteWeakGlobalRef(o);
- // Currently, deleting an already-deleted reference is just a CheckJNI warning.
+ // Currently, deleting an already-deleted reference is just a CheckJNI abort.
{
- bool old_check_jni = vm_->SetCheckJniEnabled(false);
- {
- CheckJniAbortCatcher check_jni_abort_catcher;
- env_->DeleteWeakGlobalRef(o);
- }
+ bool old_check_jni = vm_->SetCheckJniEnabled(true);
CheckJniAbortCatcher check_jni_abort_catcher;
- EXPECT_FALSE(vm_->SetCheckJniEnabled(true));
env_->DeleteWeakGlobalRef(o);
- std::string expected(StringPrintf("use of deleted weak global reference %p", o));
+ std::string expected(StringPrintf("jobject is an invalid weak global reference: %p", o));
check_jni_abort_catcher.Check(expected.c_str());
EXPECT_TRUE(vm_->SetCheckJniEnabled(old_check_jni));
}
diff --git a/runtime/thread.cc b/runtime/thread.cc
index 55844109d2..645c51f73b 100644
--- a/runtime/thread.cc
+++ b/runtime/thread.cc
@@ -2571,17 +2571,10 @@ ObjPtr<mirror::Object> Thread::DecodeJObject(jobject obj) const {
// Local references do not need a read barrier.
result = locals.Get<kWithoutReadBarrier>(ref);
} else if (kind == kHandleScopeOrInvalid) {
- // TODO: make stack indirect reference table lookup more efficient.
- // Check if this is a local reference in the handle scope.
- if (LIKELY(HandleScopeContains(obj))) {
- // Read from handle scope.
- result = reinterpret_cast<StackReference<mirror::Object>*>(obj)->AsMirrorPtr();
- VerifyObject(result);
- } else {
- tlsPtr_.jni_env->vm_->JniAbortF(nullptr, "use of invalid jobject %p", obj);
- expect_null = true;
- result = nullptr;
- }
+ // Read from handle scope.
+ DCHECK(HandleScopeContains(obj));
+ result = reinterpret_cast<StackReference<mirror::Object>*>(obj)->AsMirrorPtr();
+ VerifyObject(result);
} else if (kind == kGlobal) {
result = tlsPtr_.jni_env->vm_->DecodeGlobal(ref);
} else {
@@ -2594,10 +2587,9 @@ ObjPtr<mirror::Object> Thread::DecodeJObject(jobject obj) const {
}
}
- if (UNLIKELY(!expect_null && result == nullptr)) {
- tlsPtr_.jni_env->vm_->JniAbortF(nullptr, "use of deleted %s %p",
- ToStr<IndirectRefKind>(kind).c_str(), obj);
- }
+ DCHECK(expect_null || result != nullptr)
+ << "use of deleted " << ToStr<IndirectRefKind>(kind).c_str()
+ << " " << static_cast<const void*>(obj);
return result;
}