From 5a5311a9c0ac0147fb49ed990f4cbb4f6c7ac0a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deluan=20Quint=C3=A3o?= Date: Sat, 8 Aug 2026 21:57:44 -0400 Subject: [PATCH] fix(plugins): make generated mock stubs nil-safe for nilable returns (#5909) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ndpgen mock accessors asserted return types unconditionally, so a plugin test using Return(nil, ...) — the natural way to model an empty result or a pagination terminal page — panicked on the untyped-nil type assertion instead of returning the zero value. Guard pointer, slice, map and any returns in both the host-service client stubs and the PDK stub, and regenerate. --- plugins/cmd/ndpgen/internal/generator.go | 62 ++++++++---- plugins/cmd/ndpgen/internal/generator_test.go | 96 ++++++++++++++++++- .../internal/templates/client_stub.go.tmpl | 2 +- .../internal/templates/pdk_stub.go.tmpl | 2 +- plugins/pdk/go/host/mock_nil_returns_test.go | 27 ++++++ plugins/pdk/go/host/nd_host_cache_stub.go | 6 +- plugins/pdk/go/host/nd_host_config_stub.go | 6 +- plugins/pdk/go/host/nd_host_http_stub.go | 6 +- plugins/pdk/go/host/nd_host_kvstore_stub.go | 18 +++- plugins/pdk/go/host/nd_host_library_stub.go | 12 ++- plugins/pdk/go/host/nd_host_matcher_stub.go | 6 +- .../pdk/go/host/nd_host_subsonicapi_stub.go | 6 +- plugins/pdk/go/host/nd_host_task_stub.go | 6 +- plugins/pdk/go/host/nd_host_users_stub.go | 12 ++- plugins/pdk/go/pdk/pdk_stub.go | 24 ++++- 15 files changed, 250 insertions(+), 41 deletions(-) create mode 100644 plugins/pdk/go/host/mock_nil_returns_test.go diff --git a/plugins/cmd/ndpgen/internal/generator.go b/plugins/cmd/ndpgen/internal/generator.go index 50f53cd37..bfc1b7c4e 100644 --- a/plugins/cmd/ndpgen/internal/generator.go +++ b/plugins/cmd/ndpgen/internal/generator.go @@ -27,32 +27,46 @@ func hostFuncMap(svc Service) template.FuncMap { // Uses private (lowercase) type names for request/response structs. func clientFuncMap(svc Service) template.FuncMap { return template.FuncMap{ - "lower": strings.ToLower, - "title": strings.Title, - "exportName": func(m Method) string { return m.FunctionName(svc.ExportPrefix()) }, - "requestType": func(m Method) string { return m.ClientRequestTypeName(svc.Name) }, - "responseType": func(m Method) string { return m.ClientResponseTypeName(svc.Name) }, - "formatDoc": formatDoc, - "mockReturnValues": mockReturnValues, + "lower": strings.ToLower, + "title": strings.Title, + "exportName": func(m Method) string { return m.FunctionName(svc.ExportPrefix()) }, + "requestType": func(m Method) string { return m.ClientRequestTypeName(svc.Name) }, + "responseType": func(m Method) string { return m.ClientResponseTypeName(svc.Name) }, + "formatDoc": formatDoc, + "mockReturnBody": mockReturnBody, } } -// mockReturnValues generates the testify mock return value accessors for a method. -// For example: args.String(0), args.Bool(1), args.Error(2) -func mockReturnValues(m Method) string { +// mockReturnBody generates the testify mock body lines that extract return values. +// Nil-able returns are guarded so tests can use Return(nil, ...) without the +// type-assertion panic testify produces on an untyped nil. +func mockReturnBody(m Method) string { + var b strings.Builder var parts []string - idx := 0 - for _, r := range m.Returns { - parts = append(parts, mockAccessor(r.Type, idx)) - idx++ + for idx, r := range m.Returns { + if isNilableType(r.Type) { + name := fmt.Sprintf("r%d", idx) + fmt.Fprintf(&b, "\tvar %s %s\n\tif v := args.Get(%d); v != nil {\n\t\t%s = v.(%s)\n\t}\n", name, r.Type, idx, name, r.Type) + parts = append(parts, name) + } else { + parts = append(parts, mockAccessor(r.Type, idx)) + } } if m.HasError { - parts = append(parts, fmt.Sprintf("args.Error(%d)", idx)) + parts = append(parts, fmt.Sprintf("args.Error(%d)", len(m.Returns))) } - return strings.Join(parts, ", ") + b.WriteString("\treturn " + strings.Join(parts, ", ")) + return b.String() +} + +// isNilableType reports whether a nil mock return value is a valid intent for the +// type, rather than a malformed test expectation. +func isNilableType(typ string) bool { + return strings.HasPrefix(typ, "*") || strings.HasPrefix(typ, "[]") || + strings.HasPrefix(typ, "map[") || typ == "any" || typ == "interface{}" } // mockAccessor returns the testify mock accessor call for a given type and index. @@ -725,7 +739,7 @@ func pdkFuncMap() template.FuncMap { "returnList": pdkReturnList, "argList": pdkArgList, "argListWithReceiver": pdkArgListWithReceiver, - "mockReturns": pdkMockReturns, + "mockReturnBody": pdkMockReturnBody, "constValue": pdkConstValue, "stubTypeUnderlying": stubTypeUnderlying, "methodReceiver": pdkMethodReceiver, @@ -834,12 +848,20 @@ func pdkMethodReceiver(receiver, typeName string) string { } // pdkMockReturns generates the mock return accessors for a function. -func pdkMockReturns(returns []PDKReturn) string { +func pdkMockReturnBody(returns []PDKReturn) string { + var b strings.Builder var parts []string for i, r := range returns { - parts = append(parts, mockAccessorForType(r.Type, i)) + if isNilableType(r.Type) { + name := fmt.Sprintf("r%d", i) + fmt.Fprintf(&b, "\tvar %s %s\n\tif v := args.Get(%d); v != nil {\n\t\t%s = v.(%s)\n\t}\n", name, r.Type, i, name, r.Type) + parts = append(parts, name) + } else { + parts = append(parts, mockAccessorForType(r.Type, i)) + } } - return strings.Join(parts, ", ") + b.WriteString("\treturn " + strings.Join(parts, ", ")) + return b.String() } // mockAccessorForType returns the testify mock accessor for a type. diff --git a/plugins/cmd/ndpgen/internal/generator_test.go b/plugins/cmd/ndpgen/internal/generator_test.go index 15b97e1ae..ea94c7fc6 100644 --- a/plugins/cmd/ndpgen/internal/generator_test.go +++ b/plugins/cmd/ndpgen/internal/generator_test.go @@ -748,8 +748,9 @@ var _ = Describe("Generator", func() { // Check bool return uses args.Bool(1) Expect(codeStr).To(ContainSubstring("args.Bool(1)")) - // Check []byte return uses args.Get(0).([]byte) - Expect(codeStr).To(ContainSubstring("args.Get(0).([]byte)")) + // []byte is nil-able, so it gets a guarded extraction + Expect(codeStr).To(ContainSubstring("var r0 []byte")) + Expect(codeStr).To(ContainSubstring("r0 = v.([]byte)")) // Check error returns use args.Error(N) Expect(codeStr).To(ContainSubstring("args.Error(")) @@ -1699,6 +1700,97 @@ var _ = Describe("Rust Generation", func() { Expect(codeStr).NotTo(ContainSubstring("use base64")) }) }) + + Describe("nil-safe mock accessors", func() { + Describe("GenerateClientGoStub", func() { + It("guards pointer, slice and map returns so Return(nil, ...) does not panic", func() { + svc := Service{ + Name: "Paged", + Interface: "PagedService", + Methods: []Method{ + { + Name: "GetPage", + HasError: true, + Params: []Param{NewParam("query", "string")}, + Returns: []Param{NewParam("items", "[]Item"), NewParam("next", "*PageOptions")}, + }, + { + Name: "GetLabels", + HasError: true, + Params: []Param{NewParam("id", "string")}, + Returns: []Param{NewParam("labels", "map[string]string")}, + }, + }, + } + + code, err := GenerateClientGoStub(svc, "host") + Expect(err).NotTo(HaveOccurred()) + _, err = format.Source(code) + Expect(err).NotTo(HaveOccurred()) + + codeStr := string(code) + Expect(codeStr).To(ContainSubstring("var r0 []Item")) + Expect(codeStr).To(ContainSubstring("var r1 *PageOptions")) + Expect(codeStr).To(ContainSubstring("if v := args.Get(1); v != nil {")) + Expect(codeStr).To(ContainSubstring("r1 = v.(*PageOptions)")) + Expect(codeStr).To(ContainSubstring("return r0, r1, args.Error(2)")) + Expect(codeStr).To(ContainSubstring("var r0 map[string]string")) + Expect(codeStr).NotTo(ContainSubstring("args.Get(0).([]Item)")) + Expect(codeStr).NotTo(ContainSubstring("args.Get(1).(*PageOptions)")) + }) + + It("keeps non-nilable returns as inline accessors", func() { + svc := Service{ + Name: "Counter", + Interface: "CounterService", + Methods: []Method{ + { + Name: "Count", + HasError: true, + Params: []Param{NewParam("id", "string")}, + Returns: []Param{NewParam("count", "int64"), NewParam("name", "string")}, + }, + }, + } + + code, err := GenerateClientGoStub(svc, "host") + Expect(err).NotTo(HaveOccurred()) + + codeStr := string(code) + Expect(codeStr).To(ContainSubstring("return args.Get(0).(int64), args.String(1), args.Error(2)")) + Expect(codeStr).NotTo(ContainSubstring("var r0")) + }) + }) + + Describe("GeneratePDKGoStub", func() { + It("guards pointer and slice returns", func() { + symbols := &PDKSymbols{ + Functions: []PDKFunc{ + { + Name: "NewHTTPRequest", + Params: []PDKParam{{Name: "method", Type: "HTTPMethod"}, {Name: "url", Type: "string"}}, + Returns: []PDKReturn{{Type: "*HTTPRequest"}}, + }, + { + Name: "Input", + Returns: []PDKReturn{{Type: "[]byte"}}, + }, + }, + } + + code, err := GeneratePDKGoStub(symbols) + Expect(err).NotTo(HaveOccurred()) + _, err = format.Source(code) + Expect(err).NotTo(HaveOccurred()) + + codeStr := string(code) + Expect(codeStr).To(ContainSubstring("var r0 *HTTPRequest")) + Expect(codeStr).To(ContainSubstring("r0 = v.(*HTTPRequest)")) + Expect(codeStr).To(ContainSubstring("var r0 []byte")) + Expect(codeStr).NotTo(ContainSubstring("return args.Get(0).(*HTTPRequest)")) + }) + }) + }) }) func writeFile(path, content string) error { diff --git a/plugins/cmd/ndpgen/internal/templates/client_stub.go.tmpl b/plugins/cmd/ndpgen/internal/templates/client_stub.go.tmpl index d2a8f3f27..eb5e9f844 100644 --- a/plugins/cmd/ndpgen/internal/templates/client_stub.go.tmpl +++ b/plugins/cmd/ndpgen/internal/templates/client_stub.go.tmpl @@ -51,7 +51,7 @@ var {{.Service.Name}}Mock = &mock{{.Service.Name}}Service{} // {{.Name}} is the mock method for {{$.Service.Name}}{{.Name}}. func (m *mock{{$.Service.Name}}Service) {{.Name}}({{range $i, $p := .Params}}{{if $i}}, {{end}}{{$p.Name}} {{$p.Type}}{{end}}) {{.ReturnSignature}} { args := m.Called({{range $i, $p := .Params}}{{if $i}}, {{end}}{{$p.Name}}{{end}}) - return {{mockReturnValues .}} +{{mockReturnBody .}} } // {{$.Service.Name}}{{.Name}} delegates to the mock instance. diff --git a/plugins/cmd/ndpgen/internal/templates/pdk_stub.go.tmpl b/plugins/cmd/ndpgen/internal/templates/pdk_stub.go.tmpl index 6a57a6469..2bd73f107 100644 --- a/plugins/cmd/ndpgen/internal/templates/pdk_stub.go.tmpl +++ b/plugins/cmd/ndpgen/internal/templates/pdk_stub.go.tmpl @@ -34,7 +34,7 @@ func ResetMock() { func {{.Name}}({{paramList .Params}}){{returnList .Returns}} { {{- if .Returns}} args := PDKMock.Called({{argList .Params}}) - return {{mockReturns .Returns}} +{{mockReturnBody .Returns}} {{- else}} PDKMock.Called({{argList .Params}}) {{- end}} diff --git a/plugins/pdk/go/host/mock_nil_returns_test.go b/plugins/pdk/go/host/mock_nil_returns_test.go new file mode 100644 index 000000000..ea5f334f1 --- /dev/null +++ b/plugins/pdk/go/host/mock_nil_returns_test.go @@ -0,0 +1,27 @@ +//go:build !wasip1 + +package host + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +// Return(nil, ...) models "no result"; the generated accessors must yield the +// zero value instead of panicking on the type assertion. +func TestMockNilPointerReturn(t *testing.T) { + HTTPMock.On("Send", HTTPRequest{URL: "http://nil.example"}).Return(nil, nil) + + resp, err := HTTPSend(HTTPRequest{URL: "http://nil.example"}) + require.NoError(t, err) + require.Nil(t, resp) +} + +func TestMockNilSliceReturn(t *testing.T) { + LibraryMock.On("GetAllLibraries").Return(nil, nil) + + libs, err := LibraryGetAllLibraries() + require.NoError(t, err) + require.Nil(t, libs) +} diff --git a/plugins/pdk/go/host/nd_host_cache_stub.go b/plugins/pdk/go/host/nd_host_cache_stub.go index 46bb44bcf..faf4e65d2 100644 --- a/plugins/pdk/go/host/nd_host_cache_stub.go +++ b/plugins/pdk/go/host/nd_host_cache_stub.go @@ -154,7 +154,11 @@ func CacheSetBytes(key string, value []byte, ttlSeconds int64) error { // GetBytes is the mock method for CacheGetBytes. func (m *mockCacheService) GetBytes(key string) ([]byte, bool, error) { args := m.Called(key) - return args.Get(0).([]byte), args.Bool(1), args.Error(2) + var r0 []byte + if v := args.Get(0); v != nil { + r0 = v.([]byte) + } + return r0, args.Bool(1), args.Error(2) } // CacheGetBytes delegates to the mock instance. diff --git a/plugins/pdk/go/host/nd_host_config_stub.go b/plugins/pdk/go/host/nd_host_config_stub.go index 463c29b76..0327346a3 100644 --- a/plugins/pdk/go/host/nd_host_config_stub.go +++ b/plugins/pdk/go/host/nd_host_config_stub.go @@ -59,7 +59,11 @@ func ConfigGetInt(key string) (int64, bool) { // Keys is the mock method for ConfigKeys. func (m *mockConfigService) Keys(prefix string) []string { args := m.Called(prefix) - return args.Get(0).([]string) + var r0 []string + if v := args.Get(0); v != nil { + r0 = v.([]string) + } + return r0 } // ConfigKeys delegates to the mock instance. diff --git a/plugins/pdk/go/host/nd_host_http_stub.go b/plugins/pdk/go/host/nd_host_http_stub.go index 09c4d0fcf..8706ac9e5 100644 --- a/plugins/pdk/go/host/nd_host_http_stub.go +++ b/plugins/pdk/go/host/nd_host_http_stub.go @@ -43,7 +43,11 @@ var HTTPMock = &mockHTTPService{} // Send is the mock method for HTTPSend. func (m *mockHTTPService) Send(request HTTPRequest) (*HTTPResponse, error) { args := m.Called(request) - return args.Get(0).(*HTTPResponse), args.Error(1) + var r0 *HTTPResponse + if v := args.Get(0); v != nil { + r0 = v.(*HTTPResponse) + } + return r0, args.Error(1) } // HTTPSend delegates to the mock instance. diff --git a/plugins/pdk/go/host/nd_host_kvstore_stub.go b/plugins/pdk/go/host/nd_host_kvstore_stub.go index fce038aa1..e5243860a 100644 --- a/plugins/pdk/go/host/nd_host_kvstore_stub.go +++ b/plugins/pdk/go/host/nd_host_kvstore_stub.go @@ -64,7 +64,11 @@ func KVStoreSetWithTTL(key string, value []byte, ttlSeconds int64) error { // Get is the mock method for KVStoreGet. func (m *mockKVStoreService) Get(key string) ([]byte, bool, error) { args := m.Called(key) - return args.Get(0).([]byte), args.Bool(1), args.Error(2) + var r0 []byte + if v := args.Get(0); v != nil { + r0 = v.([]byte) + } + return r0, args.Bool(1), args.Error(2) } // KVStoreGet delegates to the mock instance. @@ -81,7 +85,11 @@ func KVStoreGet(key string) ([]byte, bool, error) { // GetMany is the mock method for KVStoreGetMany. func (m *mockKVStoreService) GetMany(keys []string) (map[string][]byte, error) { args := m.Called(keys) - return args.Get(0).(map[string][]byte), args.Error(1) + var r0 map[string][]byte + if v := args.Get(0); v != nil { + r0 = v.(map[string][]byte) + } + return r0, args.Error(1) } // KVStoreGetMany delegates to the mock instance. @@ -116,7 +124,11 @@ func KVStoreHas(key string) (bool, error) { // List is the mock method for KVStoreList. func (m *mockKVStoreService) List(prefix string) ([]string, error) { args := m.Called(prefix) - return args.Get(0).([]string), args.Error(1) + var r0 []string + if v := args.Get(0); v != nil { + r0 = v.([]string) + } + return r0, args.Error(1) } // KVStoreList delegates to the mock instance. diff --git a/plugins/pdk/go/host/nd_host_library_stub.go b/plugins/pdk/go/host/nd_host_library_stub.go index 1e4c230c3..0232c49d9 100644 --- a/plugins/pdk/go/host/nd_host_library_stub.go +++ b/plugins/pdk/go/host/nd_host_library_stub.go @@ -39,7 +39,11 @@ var LibraryMock = &mockLibraryService{} // GetLibrary is the mock method for LibraryGetLibrary. func (m *mockLibraryService) GetLibrary(id int32) (*Library, error) { args := m.Called(id) - return args.Get(0).(*Library), args.Error(1) + var r0 *Library + if v := args.Get(0); v != nil { + r0 = v.(*Library) + } + return r0, args.Error(1) } // LibraryGetLibrary delegates to the mock instance. @@ -56,7 +60,11 @@ func LibraryGetLibrary(id int32) (*Library, error) { // GetAllLibraries is the mock method for LibraryGetAllLibraries. func (m *mockLibraryService) GetAllLibraries() ([]Library, error) { args := m.Called() - return args.Get(0).([]Library), args.Error(1) + var r0 []Library + if v := args.Get(0); v != nil { + r0 = v.([]Library) + } + return r0, args.Error(1) } // LibraryGetAllLibraries delegates to the mock instance. diff --git a/plugins/pdk/go/host/nd_host_matcher_stub.go b/plugins/pdk/go/host/nd_host_matcher_stub.go index 07b34bb82..ade368c06 100644 --- a/plugins/pdk/go/host/nd_host_matcher_stub.go +++ b/plugins/pdk/go/host/nd_host_matcher_stub.go @@ -31,7 +31,11 @@ var MatcherMock = &mockMatcherService{} // MatchSongs is the mock method for MatcherMatchSongs. func (m *mockMatcherService) MatchSongs(songs []types.SongRef, opts MatchOptions) ([]*types.Track, error) { args := m.Called(songs, opts) - return args.Get(0).([]*types.Track), args.Error(1) + var r0 []*types.Track + if v := args.Get(0); v != nil { + r0 = v.([]*types.Track) + } + return r0, args.Error(1) } // MatcherMatchSongs delegates to the mock instance. diff --git a/plugins/pdk/go/host/nd_host_subsonicapi_stub.go b/plugins/pdk/go/host/nd_host_subsonicapi_stub.go index 6d3a56b35..ef3d1b735 100644 --- a/plugins/pdk/go/host/nd_host_subsonicapi_stub.go +++ b/plugins/pdk/go/host/nd_host_subsonicapi_stub.go @@ -39,7 +39,11 @@ func SubsonicAPICall(uri string) (string, error) { // CallRaw is the mock method for SubsonicAPICallRaw. func (m *mockSubsonicAPIService) CallRaw(uri string) (string, []byte, error) { args := m.Called(uri) - return args.String(0), args.Get(1).([]byte), args.Error(2) + var r1 []byte + if v := args.Get(1); v != nil { + r1 = v.([]byte) + } + return args.String(0), r1, args.Error(2) } // SubsonicAPICallRaw delegates to the mock instance. diff --git a/plugins/pdk/go/host/nd_host_task_stub.go b/plugins/pdk/go/host/nd_host_task_stub.go index 286d14035..f2c45a2b5 100644 --- a/plugins/pdk/go/host/nd_host_task_stub.go +++ b/plugins/pdk/go/host/nd_host_task_stub.go @@ -70,7 +70,11 @@ func TaskEnqueue(queueName string, payload []byte) (string, error) { // Get is the mock method for TaskGet. func (m *mockTaskService) Get(taskID string) (*TaskInfo, error) { args := m.Called(taskID) - return args.Get(0).(*TaskInfo), args.Error(1) + var r0 *TaskInfo + if v := args.Get(0); v != nil { + r0 = v.(*TaskInfo) + } + return r0, args.Error(1) } // TaskGet delegates to the mock instance. diff --git a/plugins/pdk/go/host/nd_host_users_stub.go b/plugins/pdk/go/host/nd_host_users_stub.go index 8858b2109..e0f8c0939 100644 --- a/plugins/pdk/go/host/nd_host_users_stub.go +++ b/plugins/pdk/go/host/nd_host_users_stub.go @@ -33,7 +33,11 @@ var UsersMock = &mockUsersService{} // GetUsers is the mock method for UsersGetUsers. func (m *mockUsersService) GetUsers() ([]User, error) { args := m.Called() - return args.Get(0).([]User), args.Error(1) + var r0 []User + if v := args.Get(0); v != nil { + r0 = v.([]User) + } + return r0, args.Error(1) } // UsersGetUsers delegates to the mock instance. @@ -49,7 +53,11 @@ func UsersGetUsers() ([]User, error) { // GetAdmins is the mock method for UsersGetAdmins. func (m *mockUsersService) GetAdmins() ([]User, error) { args := m.Called() - return args.Get(0).([]User), args.Error(1) + var r0 []User + if v := args.Get(0); v != nil { + r0 = v.([]User) + } + return r0, args.Error(1) } // UsersGetAdmins delegates to the mock instance. diff --git a/plugins/pdk/go/pdk/pdk_stub.go b/plugins/pdk/go/pdk/pdk_stub.go index 3bdbb1cb7..8f2707d56 100644 --- a/plugins/pdk/go/pdk/pdk_stub.go +++ b/plugins/pdk/go/pdk/pdk_stub.go @@ -62,7 +62,11 @@ func GetConfig(key string) (string, bool) { // GetVar GetVar returns the byte slice (if any) associated with `key`. func GetVar(key string) []byte { args := PDKMock.Called(key) - return args.Get(0).([]byte) + var r0 []byte + if v := args.Get(0); v != nil { + r0 = v.([]byte) + } + return r0 } // GetVarInt GetVarInt returns the int associated with `key` (or 0 if none). @@ -74,7 +78,11 @@ func GetVarInt(key string) int { // Input Input returns a slice of bytes from the host. func Input() []byte { args := PDKMock.Called() - return args.Get(0).([]byte) + var r0 []byte + if v := args.Get(0); v != nil { + r0 = v.([]byte) + } + return r0 } // InputJSON InputJSON returns unmartialed JSON data from the host "input". @@ -108,7 +116,11 @@ func LogMemory(level LogLevel, m Memory) { // NewHTTPRequest NewHTTPRequest returns a new `HTTPRequest`. func NewHTTPRequest(method HTTPMethod, url string) *HTTPRequest { args := PDKMock.Called(method, url) - return args.Get(0).(*HTTPRequest) + var r0 *HTTPRequest + if v := args.Get(0); v != nil { + r0 = v.(*HTTPRequest) + } + return r0 } func NewMemory(offset uint64, length uint64) Memory { args := PDKMock.Called(offset, length) @@ -139,7 +151,11 @@ func OutputString(s string) { // ParamBytes ParamBytes returns bytes from Extism host memory given an offset. func ParamBytes(offset uint64) []byte { args := PDKMock.Called(offset) - return args.Get(0).([]byte) + var r0 []byte + if v := args.Get(0); v != nil { + r0 = v.([]byte) + } + return r0 } // ParamString ParamString returns UTF-8 string data from Extism host memory given an offset.