diff --git a/bindings/c/include/opendal.h b/bindings/c/include/opendal.h index 4b8d95ba57c2..507d50160247 100644 --- a/bindings/c/include/opendal.h +++ b/bindings/c/include/opendal.h @@ -1375,7 +1375,7 @@ struct opendal_error *opendal_operator_check(const struct opendal_operator *op); * assert(!strcmp(scheme, "memory")); * * /// free the heap memory - * free(scheme); + * opendal_string_free(scheme); * opendal_operator_info_free(info); * ``` */ @@ -1389,14 +1389,14 @@ void opendal_operator_info_free(struct opendal_operator_info *ptr); /** * \brief Return the nul-terminated operator's scheme, i.e. service * - * \note: The string is on heap, remember to free it + * \note: The string is on heap, free it with opendal_string_free() */ char *opendal_operator_info_get_scheme(const struct opendal_operator_info *self); /** * \brief Return the nul-terminated operator's working root path * - * \note: The string is on heap, remember to free it + * \note: The string is on heap, free it with opendal_string_free() */ char *opendal_operator_info_get_root(const struct opendal_operator_info *self); @@ -1404,7 +1404,7 @@ char *opendal_operator_info_get_root(const struct opendal_operator_info *self); * \brief Return the nul-terminated operator backend's name, could be empty if underlying backend has no * namespace concept. * - * \note: The string is on heap, remember to free it + * \note: The string is on heap, free it with opendal_string_free() */ char *opendal_operator_info_get_name(const struct opendal_operator_info *self); @@ -1471,6 +1471,13 @@ uintptr_t opendal_presigned_request_headers_len(const struct opendal_presigned_r */ void opendal_presigned_request_free(struct opendal_presigned_request *req); +/** + * \brief Frees a heap-allocated string returned by OpenDAL C APIs. + * + * \note Only pass pointers returned from OpenDAL APIs that transfer string ownership. + */ +void opendal_string_free(char *ptr); + /** * \brief Frees the heap memory used by the opendal_bytes */ @@ -1519,7 +1526,7 @@ void opendal_operator_options_free(struct opendal_operator_options *ptr); * * Path is relative to operator's root. Only valid in current operator. * - * \note To free the string, you can directly call free() + * \note Free the returned string with opendal_string_free() */ char *opendal_entry_path(const struct opendal_entry *self); @@ -1530,7 +1537,7 @@ char *opendal_entry_path(const struct opendal_entry *self); * If this entry is a dir, `Name` MUST endswith `/` * Otherwise, `Name` MUST NOT endswith `/`. * - * \note To free the string, you can directly call free() + * \note Free the returned string with opendal_string_free() */ char *opendal_entry_name(const struct opendal_entry *self); diff --git a/bindings/c/src/entry.rs b/bindings/c/src/entry.rs index fc92b0f35a04..8dea50192bbb 100644 --- a/bindings/c/src/entry.rs +++ b/bindings/c/src/entry.rs @@ -53,7 +53,7 @@ impl opendal_entry { /// /// Path is relative to operator's root. Only valid in current operator. /// - /// \note To free the string, you can directly call free() + /// \note Free the returned string with opendal_string_free() #[no_mangle] pub unsafe extern "C" fn opendal_entry_path(&self) -> *mut c_char { let s = self.deref().path(); @@ -67,7 +67,7 @@ impl opendal_entry { /// If this entry is a dir, `Name` MUST endswith `/` /// Otherwise, `Name` MUST NOT endswith `/`. /// - /// \note To free the string, you can directly call free() + /// \note Free the returned string with opendal_string_free() #[no_mangle] pub unsafe extern "C" fn opendal_entry_name(&self) -> *mut c_char { let s = self.deref().name(); diff --git a/bindings/c/src/operator_info.rs b/bindings/c/src/operator_info.rs index f78fcde7bfcc..796ea9be2675 100644 --- a/bindings/c/src/operator_info.rs +++ b/bindings/c/src/operator_info.rs @@ -146,7 +146,7 @@ impl opendal_operator_info { /// assert(!strcmp(scheme, "memory")); /// /// /// free the heap memory - /// free(scheme); + /// opendal_string_free(scheme); /// opendal_operator_info_free(info); /// ``` #[no_mangle] @@ -170,7 +170,7 @@ impl opendal_operator_info { /// \brief Return the nul-terminated operator's scheme, i.e. service /// - /// \note: The string is on heap, remember to free it + /// \note: The string is on heap, free it with opendal_string_free() #[no_mangle] pub unsafe extern "C" fn opendal_operator_info_get_scheme(&self) -> *mut c_char { let scheme = self.deref().scheme().to_string(); @@ -181,7 +181,7 @@ impl opendal_operator_info { /// \brief Return the nul-terminated operator's working root path /// - /// \note: The string is on heap, remember to free it + /// \note: The string is on heap, free it with opendal_string_free() #[no_mangle] pub unsafe extern "C" fn opendal_operator_info_get_root(&self) -> *mut c_char { let root = self.deref().root(); @@ -193,7 +193,7 @@ impl opendal_operator_info { /// \brief Return the nul-terminated operator backend's name, could be empty if underlying backend has no /// namespace concept. /// - /// \note: The string is on heap, remember to free it + /// \note: The string is on heap, free it with opendal_string_free() #[no_mangle] pub unsafe extern "C" fn opendal_operator_info_get_name(&self) -> *mut c_char { let name = self.deref().name(); diff --git a/bindings/c/src/types.rs b/bindings/c/src/types.rs index a37c41930d01..2ba5b6a5bcb6 100644 --- a/bindings/c/src/types.rs +++ b/bindings/c/src/types.rs @@ -16,11 +16,21 @@ // under the License. use std::collections::HashMap; -use std::ffi::c_void; +use std::ffi::{c_void, CString}; use std::os::raw::c_char; use opendal::Buffer; +/// \brief Frees a heap-allocated string returned by OpenDAL C APIs. +/// +/// \note Only pass pointers returned from OpenDAL APIs that transfer string ownership. +#[no_mangle] +pub unsafe extern "C" fn opendal_string_free(ptr: *mut c_char) { + if !ptr.is_null() { + drop(unsafe { CString::from_raw(ptr) }); + } +} + /// \brief opendal_bytes carries raw-bytes with its length /// /// The opendal_bytes type is a C-compatible substitute for Vec type diff --git a/bindings/c/tests/list.cpp b/bindings/c/tests/list.cpp index 533fda7a8135..391bc1e460d9 100644 --- a/bindings/c/tests/list.cpp +++ b/bindings/c/tests/list.cpp @@ -95,7 +95,7 @@ TEST_F(OpendalListTest, ListDirTest) EXPECT_EQ(opendal_metadata_content_length(s.meta), nbytes); } - free(de_path); + opendal_string_free(de_path); opendal_metadata_free(s.meta); opendal_entry_free(entry); diff --git a/bindings/c/tests/opinfo.cpp b/bindings/c/tests/opinfo.cpp index 398608ec4972..e54f96c4ca75 100644 --- a/bindings/c/tests/opinfo.cpp +++ b/bindings/c/tests/opinfo.cpp @@ -92,6 +92,6 @@ TEST_F(OpendalOperatorInfoTest, InfoTest) EXPECT_TRUE(!strcmp(root, this->root.c_str())); // remember to free the strings - free(scheme); - free(root); + opendal_string_free(scheme); + opendal_string_free(root); } diff --git a/bindings/c/tests/test_suites_list.cpp b/bindings/c/tests/test_suites_list.cpp index 4c6e566154c2..cb2d853b00ad 100644 --- a/bindings/c/tests/test_suites_list.cpp +++ b/bindings/c/tests/test_suites_list.cpp @@ -69,7 +69,7 @@ void test_list_basic(opendal_test_context* ctx) OPENDAL_ASSERT_NOT_NULL(path, "Entry path should not be null"); found_paths.insert(std::string(path)); - free(path); + opendal_string_free(path); opendal_entry_free(next_result.entry); } @@ -124,7 +124,7 @@ void test_list_empty_dir(opendal_test_context* ctx) char* path = opendal_entry_path(next_result.entry); found_paths.insert(std::string(path)); - free(path); + opendal_string_free(path); opendal_entry_free(next_result.entry); } @@ -191,7 +191,7 @@ void test_list_nested(opendal_test_context* ctx) char* path = opendal_entry_path(next_result.entry); found_paths.insert(std::string(path)); - free(path); + opendal_string_free(path); opendal_entry_free(next_result.entry); } @@ -269,8 +269,8 @@ void test_entry_name_path(opendal_test_context* ctx) "Entry path should be the full path"); } - free(path); - free(name); + opendal_string_free(path); + opendal_string_free(name); opendal_entry_free(next_result.entry); } diff --git a/bindings/go/lister.go b/bindings/go/lister.go index 0d24fbc77a3d..420bb9eeab88 100644 --- a/bindings/go/lister.go +++ b/bindings/go/lister.go @@ -366,32 +366,38 @@ var ffiEntryFree = newFFI(ffiOpts{ } }) -var ffiEntryName = newFFI(ffiOpts{ - sym: "opendal_entry_name", - rType: &ffi.TypePointer, - aTypes: []*ffi.Type{&ffi.TypePointer}, -}, func(ctx context.Context, ffiCall ffiCall) func(e *opendalEntry) string { - return func(e *opendalEntry) string { - var bytePtr *byte - ffiCall( - unsafe.Pointer(&bytePtr), - unsafe.Pointer(&e), - ) - return BytePtrToString(bytePtr) - } -}) +var ffiEntryName = func() *FFI[func(e *opendalEntry) string] { + _ = ffiStringFree + return newFFI(ffiOpts{ + sym: "opendal_entry_name", + rType: &ffi.TypePointer, + aTypes: []*ffi.Type{&ffi.TypePointer}, + }, func(ctx context.Context, ffiCall ffiCall) func(e *opendalEntry) string { + return func(e *opendalEntry) string { + var bytePtr *byte + ffiCall( + unsafe.Pointer(&bytePtr), + unsafe.Pointer(&e), + ) + return copyCStringAndFree(bytePtr, ffiStringFree.symbol(ctx)) + } + }) +}() -var ffiEntryPath = newFFI(ffiOpts{ - sym: "opendal_entry_path", - rType: &ffi.TypePointer, - aTypes: []*ffi.Type{&ffi.TypePointer}, -}, func(ctx context.Context, ffiCall ffiCall) func(e *opendalEntry) string { - return func(e *opendalEntry) string { - var bytePtr *byte - ffiCall( - unsafe.Pointer(&bytePtr), - unsafe.Pointer(&e), - ) - return BytePtrToString(bytePtr) - } -}) +var ffiEntryPath = func() *FFI[func(e *opendalEntry) string] { + _ = ffiStringFree + return newFFI(ffiOpts{ + sym: "opendal_entry_path", + rType: &ffi.TypePointer, + aTypes: []*ffi.Type{&ffi.TypePointer}, + }, func(ctx context.Context, ffiCall ffiCall) func(e *opendalEntry) string { + return func(e *opendalEntry) string { + var bytePtr *byte + ffiCall( + unsafe.Pointer(&bytePtr), + unsafe.Pointer(&e), + ) + return copyCStringAndFree(bytePtr, ffiStringFree.symbol(ctx)) + } + }) +}() diff --git a/bindings/go/operator_info.go b/bindings/go/operator_info.go index 2438ec89011a..bdf30d83d364 100644 --- a/bindings/go/operator_info.go +++ b/bindings/go/operator_info.go @@ -289,47 +289,56 @@ var ffiOperatorInfoGetNativeCapability = newFFI(ffiOpts{ } }) -var ffiOperatorInfoGetScheme = newFFI(ffiOpts{ - sym: "opendal_operator_info_get_scheme", - rType: &ffi.TypePointer, - aTypes: []*ffi.Type{&ffi.TypePointer}, -}, func(ctx context.Context, ffiCall ffiCall) func(info *opendalOperatorInfo) string { - return func(info *opendalOperatorInfo) string { - var bytePtr *byte - ffiCall( - unsafe.Pointer(&bytePtr), - unsafe.Pointer(&info), - ) - return BytePtrToString(bytePtr) - } -}) - -var ffiOperatorInfoGetRoot = newFFI(ffiOpts{ - sym: "opendal_operator_info_get_root", - rType: &ffi.TypePointer, - aTypes: []*ffi.Type{&ffi.TypePointer}, -}, func(ctx context.Context, ffiCall ffiCall) func(info *opendalOperatorInfo) string { - return func(info *opendalOperatorInfo) string { - var bytePtr *byte - ffiCall( - unsafe.Pointer(&bytePtr), - unsafe.Pointer(&info), - ) - return BytePtrToString(bytePtr) - } -}) - -var ffiOperatorInfoGetName = newFFI(ffiOpts{ - sym: "opendal_operator_info_get_name", - rType: &ffi.TypePointer, - aTypes: []*ffi.Type{&ffi.TypePointer}, -}, func(ctx context.Context, ffiCall ffiCall) func(info *opendalOperatorInfo) string { - return func(info *opendalOperatorInfo) string { - var bytePtr *byte - ffiCall( - unsafe.Pointer(&bytePtr), - unsafe.Pointer(&info), - ) - return BytePtrToString(bytePtr) - } -}) +var ffiOperatorInfoGetScheme = func() *FFI[func(info *opendalOperatorInfo) string] { + _ = ffiStringFree + return newFFI(ffiOpts{ + sym: "opendal_operator_info_get_scheme", + rType: &ffi.TypePointer, + aTypes: []*ffi.Type{&ffi.TypePointer}, + }, func(ctx context.Context, ffiCall ffiCall) func(info *opendalOperatorInfo) string { + return func(info *opendalOperatorInfo) string { + var bytePtr *byte + ffiCall( + unsafe.Pointer(&bytePtr), + unsafe.Pointer(&info), + ) + return copyCStringAndFree(bytePtr, ffiStringFree.symbol(ctx)) + } + }) +}() + +var ffiOperatorInfoGetRoot = func() *FFI[func(info *opendalOperatorInfo) string] { + _ = ffiStringFree + return newFFI(ffiOpts{ + sym: "opendal_operator_info_get_root", + rType: &ffi.TypePointer, + aTypes: []*ffi.Type{&ffi.TypePointer}, + }, func(ctx context.Context, ffiCall ffiCall) func(info *opendalOperatorInfo) string { + return func(info *opendalOperatorInfo) string { + var bytePtr *byte + ffiCall( + unsafe.Pointer(&bytePtr), + unsafe.Pointer(&info), + ) + return copyCStringAndFree(bytePtr, ffiStringFree.symbol(ctx)) + } + }) +}() + +var ffiOperatorInfoGetName = func() *FFI[func(info *opendalOperatorInfo) string] { + _ = ffiStringFree + return newFFI(ffiOpts{ + sym: "opendal_operator_info_get_name", + rType: &ffi.TypePointer, + aTypes: []*ffi.Type{&ffi.TypePointer}, + }, func(ctx context.Context, ffiCall ffiCall) func(info *opendalOperatorInfo) string { + return func(info *opendalOperatorInfo) string { + var bytePtr *byte + ffiCall( + unsafe.Pointer(&bytePtr), + unsafe.Pointer(&info), + ) + return copyCStringAndFree(bytePtr, ffiStringFree.symbol(ctx)) + } + }) +}() diff --git a/bindings/go/string.go b/bindings/go/string.go new file mode 100644 index 000000000000..1686aeb86ff4 --- /dev/null +++ b/bindings/go/string.go @@ -0,0 +1,49 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package opendal + +import ( + "context" + "unsafe" + + "github.com/jupiterrider/ffi" +) + +func copyCStringAndFree(ptr *byte, free func(*byte)) string { + if ptr == nil { + return "" + } + + defer free(ptr) + return BytePtrToString(ptr) +} + +var ffiStringFree = newFFI(ffiOpts{ + sym: "opendal_string_free", + rType: &ffi.TypeVoid, + aTypes: []*ffi.Type{&ffi.TypePointer}, +}, func(_ context.Context, ffiCall ffiCall) func(ptr *byte) { + return func(ptr *byte) { + ffiCall( + nil, + unsafe.Pointer(&ptr), + ) + } +}) diff --git a/bindings/go/string_ownership_test.go b/bindings/go/string_ownership_test.go new file mode 100644 index 000000000000..bd2b63ab40e8 --- /dev/null +++ b/bindings/go/string_ownership_test.go @@ -0,0 +1,209 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package opendal + +import ( + "context" + "testing" + "unsafe" +) + +func TestCopyCStringAndFreeNil(t *testing.T) { + var freed int + freeCString := func(*byte) { + freed++ + } + + got := copyCStringAndFree(nil, freeCString) + if got != "" { + t.Fatalf("copyCStringAndFree(nil) = %q, want empty string", got) + } + if freed != 0 { + t.Fatalf("copyCStringAndFree(nil) freed %d pointers, want 0", freed) + } +} + +func TestOperatorInfoCopiesAndFreesOwnedStrings(t *testing.T) { + var freed []*byte + freeCString := func(ptr *byte) { + freed = append(freed, ptr) + } + + schemePtr := mustBytePtrFromString(t, "memory") + rootPtr := mustBytePtrFromString(t, "/tmp/") + namePtr := mustBytePtrFromString(t, "namespace") + infoInner := &opendalOperatorInfo{} + fullCap := &opendalCapability{stat: 1} + nativeCap := &opendalCapability{list: 1} + infoFreed := 0 + + ctx := context.Background() + ctx = context.WithValue(ctx, ffiStringFree.opts.sym, freeCString) + ctx = context.WithValue(ctx, ffiOperatorInfoNew.opts.sym, func(op *opendalOperator) *opendalOperatorInfo { + if op == nil { + t.Fatal("Info() passed nil operator") + } + return infoInner + }) + ctx = context.WithValue(ctx, ffiOperatorInfoFree.opts.sym, func(info *opendalOperatorInfo) { + if info != infoInner { + t.Fatalf("Info() freed unexpected operator info: %p", info) + } + infoFreed++ + }) + ctx = context.WithValue(ctx, ffiOperatorInfoGetScheme.opts.sym, ffiOperatorInfoGetScheme.withFunc(ctx, func(rValue unsafe.Pointer, aValues ...unsafe.Pointer) { + assertOperatorInfoPointer(t, infoInner, aValues...) + *(**byte)(rValue) = schemePtr + })) + ctx = context.WithValue(ctx, ffiOperatorInfoGetRoot.opts.sym, ffiOperatorInfoGetRoot.withFunc(ctx, func(rValue unsafe.Pointer, aValues ...unsafe.Pointer) { + assertOperatorInfoPointer(t, infoInner, aValues...) + *(**byte)(rValue) = rootPtr + })) + ctx = context.WithValue(ctx, ffiOperatorInfoGetName.opts.sym, ffiOperatorInfoGetName.withFunc(ctx, func(rValue unsafe.Pointer, aValues ...unsafe.Pointer) { + assertOperatorInfoPointer(t, infoInner, aValues...) + *(**byte)(rValue) = namePtr + })) + ctx = context.WithValue(ctx, ffiOperatorInfoGetFullCapability.opts.sym, func(info *opendalOperatorInfo) *opendalCapability { + if info != infoInner { + t.Fatalf("Info() requested full capability for unexpected operator info: %p", info) + } + return fullCap + }) + ctx = context.WithValue(ctx, ffiOperatorInfoGetNativeCapability.opts.sym, func(info *opendalOperatorInfo) *opendalCapability { + if info != infoInner { + t.Fatalf("Info() requested native capability for unexpected operator info: %p", info) + } + return nativeCap + }) + + op := &Operator{ + ctx: ctx, + inner: &opendalOperator{}, + } + + info := op.Info() + if info.GetScheme() != "memory" { + t.Fatalf("Info().GetScheme() = %q, want memory", info.GetScheme()) + } + if info.GetRoot() != "/tmp/" { + t.Fatalf("Info().GetRoot() = %q, want /tmp/", info.GetRoot()) + } + if info.GetName() != "namespace" { + t.Fatalf("Info().GetName() = %q, want namespace", info.GetName()) + } + if !info.GetFullCapability().Stat() { + t.Fatal("Info().GetFullCapability().Stat() = false, want true") + } + if !info.GetNativeCapability().List() { + t.Fatal("Info().GetNativeCapability().List() = false, want true") + } + if infoFreed != 1 { + t.Fatalf("Info() freed operator info %d times, want 1", infoFreed) + } + assertFreedPointers(t, freed, schemePtr, rootPtr, namePtr) +} + +func TestNewEntryCopiesAndFreesOwnedStrings(t *testing.T) { + var freed []*byte + freeCString := func(ptr *byte) { + freed = append(freed, ptr) + } + + namePtr := mustBytePtrFromString(t, "file.txt") + pathPtr := mustBytePtrFromString(t, "dir/file.txt") + entryInner := &opendalEntry{} + entryFreed := 0 + + ctx := context.Background() + ctx = context.WithValue(ctx, ffiStringFree.opts.sym, freeCString) + ctx = context.WithValue(ctx, ffiEntryFree.opts.sym, func(entry *opendalEntry) { + if entry != entryInner { + t.Fatalf("newEntry() freed unexpected entry: %p", entry) + } + entryFreed++ + }) + ctx = context.WithValue(ctx, ffiEntryName.opts.sym, ffiEntryName.withFunc(ctx, func(rValue unsafe.Pointer, aValues ...unsafe.Pointer) { + assertEntryPointer(t, entryInner, aValues...) + *(**byte)(rValue) = namePtr + })) + ctx = context.WithValue(ctx, ffiEntryPath.opts.sym, ffiEntryPath.withFunc(ctx, func(rValue unsafe.Pointer, aValues ...unsafe.Pointer) { + assertEntryPointer(t, entryInner, aValues...) + *(**byte)(rValue) = pathPtr + })) + + entry := newEntry(ctx, entryInner) + if entry.Name() != "file.txt" { + t.Fatalf("newEntry().Name() = %q, want file.txt", entry.Name()) + } + if entry.Path() != "dir/file.txt" { + t.Fatalf("newEntry().Path() = %q, want dir/file.txt", entry.Path()) + } + if entryFreed != 1 { + t.Fatalf("newEntry() freed entry %d times, want 1", entryFreed) + } + assertFreedPointers(t, freed, namePtr, pathPtr) +} + +func mustBytePtrFromString(t *testing.T, value string) *byte { + t.Helper() + + ptr, err := BytePtrFromString(value) + if err != nil { + t.Fatalf("BytePtrFromString(%q) failed: %v", value, err) + } + return ptr +} + +func assertOperatorInfoPointer(t *testing.T, want *opendalOperatorInfo, aValues ...unsafe.Pointer) { + t.Helper() + + if len(aValues) != 1 { + t.Fatalf("operator info getter received %d arguments, want 1", len(aValues)) + } + got := *(**opendalOperatorInfo)(aValues[0]) + if got != want { + t.Fatalf("operator info getter received %p, want %p", got, want) + } +} + +func assertEntryPointer(t *testing.T, want *opendalEntry, aValues ...unsafe.Pointer) { + t.Helper() + + if len(aValues) != 1 { + t.Fatalf("entry getter received %d arguments, want 1", len(aValues)) + } + got := *(**opendalEntry)(aValues[0]) + if got != want { + t.Fatalf("entry getter received %p, want %p", got, want) + } +} + +func assertFreedPointers(t *testing.T, got []*byte, want ...*byte) { + t.Helper() + + if len(got) != len(want) { + t.Fatalf("freed %d pointers, want %d", len(got), len(want)) + } + for i := range want { + if got[i] != want[i] { + t.Fatalf("freed pointer[%d] = %p, want %p", i, got[i], want[i]) + } + } +}