feat(plugins): generate client wrappers for host functions

Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
Deluan 2025-12-23 20:44:21 -05:00
parent a0a5168f5f
commit ba27a8ceef
21 changed files with 1377 additions and 236 deletions

1
go.mod
View File

@ -21,6 +21,7 @@ require (
github.com/djherbis/stream v1.4.0
github.com/djherbis/times v1.6.0
github.com/dustin/go-humanize v1.0.1
github.com/extism/go-pdk v1.1.3
github.com/extism/go-sdk v1.7.1
github.com/fatih/structs v1.1.0
github.com/go-chi/chi/v5 v5.2.3

2
go.sum
View File

@ -57,6 +57,8 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/dylibso/observe-sdk/go v0.0.0-20240819160327-2d926c5d788a h1:UwSIFv5g5lIvbGgtf3tVwC7Ky9rmMFBp0RMs+6f6YqE=
github.com/dylibso/observe-sdk/go v0.0.0-20240819160327-2d926c5d788a/go.mod h1:C8DzXehI4zAbrdlbtOByKX6pfivJTBiV9Jjqv56Yd9Q=
github.com/extism/go-pdk v1.1.3 h1:hfViMPWrqjN6u67cIYRALZTZLk/enSPpNKa+rZ9X2SQ=
github.com/extism/go-pdk v1.1.3/go.mod h1:Gz+LIU/YCKnKXhgge8yo5Yu1F/lbv7KtKFkiCSzW/P4=
github.com/extism/go-sdk v1.7.1 h1:lWJos6uY+tRFdlIHR+SJjwFDApY7OypS/2nMhiVQ9Sw=
github.com/extism/go-sdk v1.7.1/go.mod h1:IT+Xdg5AZM9hVtpFUA+uZCJMge/hbvshl8bwzLtFyKA=
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=

View File

@ -5,18 +5,22 @@ A code generator for Navidrome's plugin host functions. It reads Go interface de
## Usage
```bash
hostgen -input <dir> -output <dir> -package <name> [-v] [-dry-run]
hostgen -input <dir> -output <dir> -package <name> [-v] [-dry-run] [-host-only] [-plugin-only]
```
### Flags
| Flag | Description | Default |
|------------|----------------------------------------------------------------|----------|
| `-input` | Directory containing Go source files with annotated interfaces | Required |
| `-output` | Directory where generated files will be written | Required |
| `-package` | Package name for generated files | Required |
| `-v` | Verbose output | `false` |
| `-dry-run` | Parse and validate without writing files | `false` |
| Flag | Description | Default |
|----------------|----------------------------------------------------------------|----------|
| `-input` | Directory containing Go source files with annotated interfaces | Required |
| `-output` | Directory where generated files will be written | Required |
| `-package` | Package name for generated files | Required |
| `-v` | Verbose output | `false` |
| `-dry-run` | Parse and validate without writing files | `false` |
| `-host-only` | Generate only host-side wrapper code | `false` |
| `-plugin-only` | Generate only plugin/client-side wrapper code | `false` |
By default, both host and plugin code are generated. Use `-host-only` or `-plugin-only` to generate only one type.
### Example
@ -162,7 +166,9 @@ type ServiceSearchResponse struct {
## Output Files
Generated files are named `<servicename>_gen.go` (lowercase). Each file includes:
### Host Code (Navidrome-side)
Generated files are named `<servicename>_gen.go` (lowercase) and placed in the output directory. Each file includes:
- `// Code generated by hostgen. DO NOT EDIT.` header
- Required imports (`context`, `encoding/json`, `extism`)
@ -171,6 +177,25 @@ Generated files are named `<servicename>_gen.go` (lowercase). Each file includes
- Host function wrappers
- Helper functions (`writeResponse`, `writeErrorResponse`)
### Plugin/Client Code (TinyGo WASM)
Generated files are named `nd_host_<servicename>.go` (lowercase) and placed in the `go/` subdirectory of the output directory. These files are intended for use in Navidrome plugins built with TinyGo. Each file includes:
- `// Code generated by hostgen. DO NOT EDIT.` header
- Required imports (`encoding/json`, `errors`, `github.com/extism/go-pdk`)
- `//go:wasmimport` declarations for each host function
- Response struct types
- Wrapper functions that handle memory allocation and JSON parsing
### Example Output Structure
```
output/
├── subsonicapi_gen.go # Host-side code (for Navidrome)
└── go/
└── nd_host_subsonicapi.go # Plugin-side code (for TinyGo plugins)
```
## Troubleshooting
### Annotations Not Detected

View File

@ -150,69 +150,146 @@ type ServiceB interface {
Expect(filepath.Join(outputDir, "servicea_gen.go")).To(BeAnExistingFile())
Expect(filepath.Join(outputDir, "serviceb_gen.go")).To(BeAnExistingFile())
})
It("generates only host code with -host-only flag", func() {
cmd := exec.Command(hostgenBin, "-input", testDir, "-output", outputDir, "-package", "testpkg", "-host-only")
output, err := cmd.CombinedOutput()
Expect(err).ToNot(HaveOccurred(), "Command failed: %s", output)
Expect(filepath.Join(outputDir, "test_gen.go")).To(BeAnExistingFile())
Expect(filepath.Join(outputDir, "go")).ToNot(BeADirectory())
})
It("generates only client code with -plugin-only flag", func() {
cmd := exec.Command(hostgenBin, "-input", testDir, "-output", outputDir, "-package", "main", "-plugin-only")
output, err := cmd.CombinedOutput()
Expect(err).ToNot(HaveOccurred(), "Command failed: %s", output)
// Host code should not exist in output root
entries, err := os.ReadDir(outputDir)
Expect(err).ToNot(HaveOccurred())
var genFiles []string
for _, e := range entries {
if e.Name() != "go" {
genFiles = append(genFiles, e.Name())
}
}
Expect(genFiles).To(BeEmpty(), "Expected no host code files, found: %v", genFiles)
// Client code should exist in go/ subdirectory
Expect(filepath.Join(outputDir, "go")).To(BeADirectory())
Expect(filepath.Join(outputDir, "go", "nd_host_test.go")).To(BeAnExistingFile())
})
It("generates both host and client code by default", func() {
cmd := exec.Command(hostgenBin, "-input", testDir, "-output", outputDir, "-package", "testpkg")
output, err := cmd.CombinedOutput()
Expect(err).ToNot(HaveOccurred(), "Command failed: %s", output)
// Host code in output root
Expect(filepath.Join(outputDir, "test_gen.go")).To(BeAnExistingFile())
// Client code in go/ subdirectory
Expect(filepath.Join(outputDir, "go")).To(BeADirectory())
Expect(filepath.Join(outputDir, "go", "nd_host_test.go")).To(BeAnExistingFile())
})
It("rejects using both -host-only and -plugin-only together", func() {
cmd := exec.Command(hostgenBin, "-input", testDir, "-output", outputDir, "-package", "testpkg", "-host-only", "-plugin-only")
output, err := cmd.CombinedOutput()
Expect(err).To(HaveOccurred())
Expect(string(output)).To(ContainSubstring("-host-only and -plugin-only cannot be used together"))
})
})
Describe("code generation", func() {
DescribeTable("generates correct output",
func(serviceFile, expectedFile string) {
DescribeTable("generates correct host and client output",
func(serviceFile, hostExpectedFile, clientExpectedFile string) {
serviceCode := readTestdata(serviceFile)
expectedCode := readTestdata(expectedFile)
hostExpected := readTestdata(hostExpectedFile)
clientExpected := readTestdata(clientExpectedFile)
Expect(os.WriteFile(filepath.Join(testDir, "service.go"), []byte(serviceCode), 0600)).To(Succeed())
// Generate both host and client code in one run
cmd := exec.Command(hostgenBin, "-input", testDir, "-output", outputDir, "-package", "testpkg")
output, err := cmd.CombinedOutput()
Expect(err).ToNot(HaveOccurred(), "Command failed: %s", output)
// Verify host code
entries, err := os.ReadDir(outputDir)
Expect(err).ToNot(HaveOccurred())
Expect(entries).To(HaveLen(1), "Expected exactly one generated file")
actual, err := os.ReadFile(filepath.Join(outputDir, entries[0].Name()))
var hostFiles []string
for _, e := range entries {
if e.Name() != "go" && !e.IsDir() {
hostFiles = append(hostFiles, e.Name())
}
}
Expect(hostFiles).To(HaveLen(1), "Expected exactly one host file, got: %v", hostFiles)
hostActual, err := os.ReadFile(filepath.Join(outputDir, hostFiles[0]))
Expect(err).ToNot(HaveOccurred())
// Format both for comparison
formattedActual, err := format.Source(actual)
Expect(err).ToNot(HaveOccurred(), "Generated code is not valid Go:\n%s", actual)
formattedHostActual, err := format.Source(hostActual)
Expect(err).ToNot(HaveOccurred(), "Generated host code is not valid Go:\n%s", hostActual)
formattedExpected, err := format.Source([]byte(expectedCode))
Expect(err).ToNot(HaveOccurred(), "Expected code is not valid Go")
formattedHostExpected, err := format.Source([]byte(hostExpected))
Expect(err).ToNot(HaveOccurred(), "Expected host code is not valid Go")
Expect(string(formattedActual)).To(Equal(string(formattedExpected)))
Expect(string(formattedHostActual)).To(Equal(string(formattedHostExpected)), "Host code mismatch")
// Verify client code
goDir := filepath.Join(outputDir, "go")
clientEntries, err := os.ReadDir(goDir)
Expect(err).ToNot(HaveOccurred())
Expect(clientEntries).To(HaveLen(1), "Expected exactly one client file")
clientActual, err := os.ReadFile(filepath.Join(goDir, clientEntries[0].Name()))
Expect(err).ToNot(HaveOccurred())
formattedClientActual, err := format.Source(clientActual)
Expect(err).ToNot(HaveOccurred(), "Generated client code is not valid Go:\n%s", clientActual)
formattedClientExpected, err := format.Source([]byte(clientExpected))
Expect(err).ToNot(HaveOccurred(), "Expected client code is not valid Go")
Expect(string(formattedClientActual)).To(Equal(string(formattedClientExpected)), "Client code mismatch")
},
Entry("simple string params - no request type needed",
"echo_service.go", "echo_expected.go"),
Entry("simple string params",
"echo_service.go", "echo_expected.go", "echo_client_expected.go"),
Entry("multiple simple params",
"math_service.go", "math_expected.go"),
Entry("multiple simple params (int32)",
"math_service.go", "math_expected.go", "math_client_expected.go"),
Entry("struct param with request type",
"store_service.go", "store_expected.go"),
"store_service.go", "store_expected.go", "store_client_expected.go"),
Entry("mixed simple and complex params",
"list_service.go", "list_expected.go"),
"list_service.go", "list_expected.go", "list_client_expected.go"),
Entry("method without error",
"counter_service.go", "counter_expected.go"),
"counter_service.go", "counter_expected.go", "counter_client_expected.go"),
Entry("no params, error only",
"ping_service.go", "ping_expected.go"),
"ping_service.go", "ping_expected.go", "ping_client_expected.go"),
Entry("map and interface types",
"meta_service.go", "meta_expected.go"),
"meta_service.go", "meta_expected.go", "meta_client_expected.go"),
Entry("pointer types",
"users_service.go", "users_expected.go"),
"users_service.go", "users_expected.go", "users_client_expected.go"),
Entry("multiple returns",
"search_service.go", "search_expected.go"),
"search_service.go", "search_expected.go", "search_client_expected.go"),
Entry("bytes",
"codec_service.go", "codec_expected.go"),
"codec_service.go", "codec_expected.go", "codec_client_expected.go"),
)
It("generates compilable code for comprehensive service", func() {
It("generates compilable host code for comprehensive service", func() {
serviceCode := readTestdata("comprehensive_service.go")
Expect(os.WriteFile(filepath.Join(testDir, "service.go"), []byte(serviceCode), 0600)).To(Succeed())
@ -221,8 +298,8 @@ type ServiceB interface {
goMod := "module testpkg\n\ngo 1.23\n\nrequire github.com/extism/go-sdk v1.7.1\n"
Expect(os.WriteFile(filepath.Join(testDir, "go.mod"), []byte(goMod), 0600)).To(Succeed())
// Generate
cmd := exec.Command(hostgenBin, "-input", testDir, "-output", testDir, "-package", "testpkg")
// Generate host code only
cmd := exec.Command(hostgenBin, "-input", testDir, "-output", testDir, "-package", "testpkg", "-host-only")
output, err := cmd.CombinedOutput()
Expect(err).ToNot(HaveOccurred(), "Generation failed: %s", output)
@ -238,6 +315,89 @@ type ServiceB interface {
buildOutput, err := buildCmd.CombinedOutput()
Expect(err).ToNot(HaveOccurred(), "Build failed: %s", buildOutput)
})
It("generates compilable client code for comprehensive service", func() {
serviceCode := readTestdata("comprehensive_service.go")
Expect(os.WriteFile(filepath.Join(testDir, "service.go"), []byte(serviceCode), 0600)).To(Succeed())
// Generate client code only to a separate client directory
clientDir := filepath.Join(outputDir, "client")
cmd := exec.Command(hostgenBin, "-input", testDir, "-output", clientDir, "-package", "main", "-plugin-only")
output, err := cmd.CombinedOutput()
Expect(err).ToNot(HaveOccurred(), "Generation failed: %s", output)
// Read generated client code
goDir := filepath.Join(clientDir, "go")
entries, err := os.ReadDir(goDir)
Expect(err).ToNot(HaveOccurred())
Expect(entries).To(HaveLen(1), "Expected exactly one generated client file")
content, err := os.ReadFile(filepath.Join(goDir, entries[0].Name()))
Expect(err).ToNot(HaveOccurred())
// Verify key expected content first
contentStr := string(content)
// Should have wasmimport declarations for all methods
Expect(contentStr).To(ContainSubstring("//go:wasmimport extism:host/user comprehensive_simpleparams"))
Expect(contentStr).To(ContainSubstring("//go:wasmimport extism:host/user comprehensive_structparam"))
Expect(contentStr).To(ContainSubstring("//go:wasmimport extism:host/user comprehensive_noerror"))
Expect(contentStr).To(ContainSubstring("//go:wasmimport extism:host/user comprehensive_noparams"))
Expect(contentStr).To(ContainSubstring("//go:wasmimport extism:host/user comprehensive_noparamsnoreturns"))
// Should have response types for methods with complex returns
Expect(contentStr).To(ContainSubstring("type ComprehensiveSimpleParamsResponse struct"))
Expect(contentStr).To(ContainSubstring("type ComprehensiveMultipleReturnsResponse struct"))
// Should have wrapper functions
Expect(contentStr).To(ContainSubstring("func ComprehensiveSimpleParams("))
Expect(contentStr).To(ContainSubstring("func ComprehensiveNoParams()"))
Expect(contentStr).To(ContainSubstring("func ComprehensiveNoParamsNoReturns()"))
// Move generated file to clientDir root for compilation
Expect(os.Rename(filepath.Join(goDir, entries[0].Name()), filepath.Join(clientDir, "nd_host.go"))).To(Succeed())
// Create go.mod for client code
goMod := "module main\n\ngo 1.23\n\nrequire github.com/extism/go-pdk v1.1.1\n"
Expect(os.WriteFile(filepath.Join(clientDir, "go.mod"), []byte(goMod), 0600)).To(Succeed())
// Add a simple main function for the plugin
mainGo := `package main
func main() {}
`
Expect(os.WriteFile(filepath.Join(clientDir, "main.go"), []byte(mainGo), 0600)).To(Succeed())
// Add type definitions needed by the generated code
typesGo := `package main
type User2 struct {
ID string
Name string
}
type Filter2 struct {
Active bool
}
`
Expect(os.WriteFile(filepath.Join(clientDir, "types.go"), []byte(typesGo), 0600)).To(Succeed())
// Tidy dependencies
goTidyCmd := exec.Command("go", "mod", "tidy")
goTidyCmd.Dir = clientDir
goTidyOutput, err := goTidyCmd.CombinedOutput()
Expect(err).ToNot(HaveOccurred(), "go mod tidy failed: %s", goTidyOutput)
// Build as WASM plugin - this validates the client code compiles correctly
buildCmd := exec.Command("go", "build", "-buildmode=c-shared", "-o", "plugin.wasm", ".")
buildCmd.Dir = clientDir
buildCmd.Env = append(os.Environ(), "GOOS=wasip1", "GOARCH=wasm")
buildOutput, err := buildCmd.CombinedOutput()
Expect(err).ToNot(HaveOccurred(), "WASM build failed: %s", buildOutput)
// Verify .wasm file was created
Expect(filepath.Join(clientDir, "plugin.wasm")).To(BeAnExistingFile())
})
})
})

View File

@ -2,14 +2,18 @@ package internal
import (
"bytes"
"embed"
"fmt"
"strings"
"text/template"
)
// GenerateService generates the host function wrapper code for a service.
func GenerateService(svc Service, pkgName string) ([]byte, error) {
tmpl, err := template.New("service").Funcs(template.FuncMap{
//go:embed templates/*.tmpl
var templatesFS embed.FS
// hostFuncMap returns the template functions for host code generation.
func hostFuncMap(svc Service) template.FuncMap {
return template.FuncMap{
"lower": strings.ToLower,
"title": strings.Title,
"exportName": func(m Method) string { return m.FunctionName(svc.ExportPrefix()) },
@ -27,7 +31,39 @@ func GenerateService(svc Service, pkgName string) ([]byte, error) {
"readParam": generateReadParam,
"writeReturn": generateWriteReturn,
"encodeReturn": generateEncodeReturn,
}).Parse(serviceTemplate)
}
}
// clientFuncMap returns the template functions for client code generation.
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()) },
"responseType": func(m Method) string { return m.ResponseTypeName(svc.Name) },
"isSimple": IsSimpleType,
"isString": IsStringType,
"isBytes": IsBytesType,
"needsJSON": NeedsJSON,
"needsRespType": func(m Method) bool { return m.NeedsResponseType() },
"isErrorOnly": func(m Method) bool { return m.IsErrorOnly() },
"wasmParamType": wasmParamType,
"wasmReturnType": wasmReturnType,
"wrapperReturnType": func(m Method, svcName string) string { return wrapperReturnType(m, svcName) },
"clientCallArg": clientCallArg,
"decodeResult": decodeResult,
"formatDoc": formatDoc,
}
}
// GenerateHost generates the host function wrapper code for a service.
func GenerateHost(svc Service, pkgName string) ([]byte, error) {
tmplContent, err := templatesFS.ReadFile("templates/host.go.tmpl")
if err != nil {
return nil, fmt.Errorf("reading host template: %w", err)
}
tmpl, err := template.New("host").Funcs(hostFuncMap(svc)).Parse(string(tmplContent))
if err != nil {
return nil, fmt.Errorf("parsing template: %w", err)
}
@ -48,10 +84,43 @@ func GenerateService(svc Service, pkgName string) ([]byte, error) {
return buf.Bytes(), nil
}
// GenerateService generates the host function wrapper code for a service.
// Deprecated: Use GenerateHost instead.
func GenerateService(svc Service, pkgName string) ([]byte, error) {
return GenerateHost(svc, pkgName)
}
// GenerateClientGo generates client wrapper code for plugins to call host functions.
func GenerateClientGo(svc Service) ([]byte, error) {
tmplContent, err := templatesFS.ReadFile("templates/client_go.go.tmpl")
if err != nil {
return nil, fmt.Errorf("reading client template: %w", err)
}
tmpl, err := template.New("client").Funcs(clientFuncMap(svc)).Parse(string(tmplContent))
if err != nil {
return nil, fmt.Errorf("parsing template: %w", err)
}
data := templateData{
Service: svc,
NeedsJSON: serviceClientNeedsJSON(svc),
NeedsErrors: serviceClientNeedsErrors(svc),
}
var buf bytes.Buffer
if err := tmpl.Execute(&buf, data); err != nil {
return nil, fmt.Errorf("executing template: %w", err)
}
return buf.Bytes(), nil
}
type templateData struct {
Package string
Service Service
NeedsJSON bool
NeedsErrors bool // Client: needs "errors" import
NeedsWriteHelper bool
NeedsErrorHelper bool
}
@ -87,6 +156,35 @@ func serviceNeedsWriteHelper(svc Service) bool {
return false
}
// serviceClientNeedsJSON returns true if any method needs JSON encoding in client code.
// This is true if any method has a response type (complex returns) or if any param/return needs JSON.
func serviceClientNeedsJSON(svc Service) bool {
for _, m := range svc.Methods {
// Response types use JSON for serialization
if m.NeedsResponseType() {
return true
}
// Parameters that need JSON marshaling
for _, p := range m.Params {
if NeedsJSON(p.Type) {
return true
}
}
}
return false
}
// serviceClientNeedsErrors returns true if any method needs the errors package in client code.
// This is true if any method returns an error.
func serviceClientNeedsErrors(svc Service) bool {
for _, m := range svc.Methods {
if m.HasError {
return true
}
}
return false
}
// serviceNeedsErrorHelper returns true if any method needs error handling with JSON.
func serviceNeedsErrorHelper(svc Service) bool {
for _, m := range svc.Methods {
@ -206,165 +304,94 @@ func generateEncodeReturn(p Param, varName string) string {
}
}
const serviceTemplate = `// Code generated by hostgen. DO NOT EDIT.
// Client-side helper functions for template
package {{.Package}}
import (
"context"
{{- if .NeedsJSON}}
"encoding/json"
{{- end}}
extism "github.com/extism/go-sdk"
)
{{- /* Generate request/response types only when needed */ -}}
{{range .Service.Methods}}
{{- if needsRequestType .}}
// {{requestType .}} is the request type for {{$.Service.Name}}.{{.Name}}.
type {{requestType .}} struct {
{{- range .Params}}
{{title .Name}} {{.Type}} ` + "`" + `json:"{{.JSONName}}"` + "`" + `
{{- end}}
// wasmParamType returns the WASM parameter type for a Go parameter.
func wasmParamType(p Param) string {
if IsSimpleType(p.Type) {
return p.Type
}
// All pointer types (string, []byte, complex) use uint64 offset
return "uint64"
}
{{- end}}
{{- if needsRespType .}}
// {{responseType .}} is the response type for {{$.Service.Name}}.{{.Name}}.
type {{responseType .}} struct {
{{- range .Returns}}
{{title .Name}} {{.Type}} ` + "`" + `json:"{{.JSONName}},omitempty"` + "`" + `
{{- end}}
Error string ` + "`" + `json:"error,omitempty"` + "`" + `
// wasmReturnType returns the WASM return type declaration for a method.
func wasmReturnType(m Method) string {
// Methods with JSON responses or error-only return uint64 (pointer)
if m.NeedsResponseType() || m.IsErrorOnly() {
return "uint64"
}
// Simple return types
if len(m.Returns) == 1 && IsSimpleType(m.Returns[0].Type) {
return m.Returns[0].Type
}
// No returns or multiple returns - use uint64 for pointer
if len(m.Returns) == 0 {
return ""
}
return "uint64"
}
{{- end}}
{{end}}
// Register{{.Service.Name}}HostFunctions registers {{.Service.Name}} service host functions.
// The returned host functions should be added to the plugin's configuration.
func Register{{.Service.Name}}HostFunctions(service {{.Service.Interface}}) []extism.HostFunction {
return []extism.HostFunction{
{{- range .Service.Methods}}
new{{$.Service.Name}}{{.Name}}HostFunction(service),
{{- end}}
// wrapperReturnType returns the Go return type for the wrapper function.
func wrapperReturnType(m Method, svcName string) string {
if m.NeedsResponseType() {
return fmt.Sprintf("(*%s%sResponse, error)", svcName, m.Name)
}
if m.IsErrorOnly() {
return "error"
}
if len(m.Returns) == 1 {
return m.Returns[0].Type
}
return ""
}
// clientCallArg returns the argument expression for calling the host function.
func clientCallArg(p Param) string {
if IsSimpleType(p.Type) {
return p.Name
}
// Pointer types use .Offset()
return p.Name + "Mem.Offset()"
}
// decodeResult generates code to decode a simple return value.
func decodeResult(p Param, varName string) string {
switch p.Type {
case "int32":
return fmt.Sprintf("int32(%s)", varName)
case "uint32":
return fmt.Sprintf("uint32(%s)", varName)
case "int64":
return fmt.Sprintf("int64(%s)", varName)
case "uint64":
return varName
case "float32":
return fmt.Sprintf("math.Float32frombits(uint32(%s))", varName)
case "float64":
return fmt.Sprintf("math.Float64frombits(%s)", varName)
case "bool":
return fmt.Sprintf("%s != 0", varName)
case "string":
// pdk.FindMemory returns a value type, ReadBytes has pointer receiver
return fmt.Sprintf("func() string { m := pdk.FindMemory(%s); return string(m.ReadBytes()) }()", varName)
case "[]byte":
return fmt.Sprintf("func() []byte { m := pdk.FindMemory(%s); return m.ReadBytes() }()", varName)
default:
return varName
}
}
{{range .Service.Methods}}
func new{{$.Service.Name}}{{.Name}}HostFunction(service {{$.Service.Interface}}) extism.HostFunction {
return extism.NewHostFunctionWithStack(
"{{exportName .}}",
func(ctx context.Context, p *extism.CurrentPlugin, stack []uint64) {
{{- if .HasParams}}
{{- if needsRequestType .}}
// Read JSON request from plugin memory
reqBytes, err := p.ReadBytes(stack[0])
if err != nil {
{{$.Service.Name | lower}}WriteError(p, stack, err)
return
}
var req {{requestType .}}
if err := json.Unmarshal(reqBytes, &req); err != nil {
{{$.Service.Name | lower}}WriteError(p, stack, err)
return
}
{{- else}}
// Read parameters from stack
{{- range $i, $p := .Params}}
{{readParam $p $i}}
{{- end}}
{{- end}}
{{- end}}
// Call the service method
{{- $m := .}}
{{- if .HasReturns}}
{{- if .HasError}}
{{range $i, $r := .Returns}}{{if $i}}, {{end}}{{lower $r.Name}}{{end}}, err := service.{{.Name}}(ctx{{range .Params}}, {{if needsRequestType $m}}req.{{title .Name}}{{else}}{{.Name}}{{end}}{{end}})
{{- else}}
{{range $i, $r := .Returns}}{{if $i}}, {{end}}{{lower $r.Name}}{{end}} := service.{{.Name}}(ctx{{range .Params}}, {{if needsRequestType $m}}req.{{title .Name}}{{else}}{{.Name}}{{end}}{{end}})
{{- end}}
{{- else if .HasError}}
err {{if hasErrFromRead .}}={{else}}:={{end}} service.{{.Name}}(ctx{{range .Params}}, {{if needsRequestType $m}}req.{{title .Name}}{{else}}{{.Name}}{{end}}{{end}})
{{- else}}
service.{{.Name}}(ctx{{range .Params}}, {{if needsRequestType $m}}req.{{title .Name}}{{else}}{{.Name}}{{end}}{{end}})
{{- end}}
{{- if .HasError}}
if err != nil {
{{- if isErrorOnly .}}
// Write error string to plugin memory
if ptr, err := p.WriteString(err.Error()); err == nil {
stack[0] = ptr
}
{{- else if needsRespType .}}
{{$.Service.Name | lower}}WriteError(p, stack, err)
{{- end}}
return
}
{{- end}}
{{- if isErrorOnly .}}
// Write empty string to indicate success
if ptr, err := p.WriteString(""); err == nil {
stack[0] = ptr
}
{{- else if needsRespType .}}
// Write JSON response to plugin memory
resp := {{responseType .}}{
{{- range .Returns}}
{{title .Name}}: {{lower .Name}},
{{- end}}
}
{{$.Service.Name | lower}}WriteResponse(p, stack, resp)
{{- else if .HasReturns}}
// Write return values to stack
{{- range $i, $r := .Returns}}
{{writeReturn $r $i (lower $r.Name)}}
{{- end}}
{{- end}}
},
{{- if needsRequestType $m}}
[]extism.ValueType{extism.ValueTypePTR},
{{- else}}
[]extism.ValueType{ {{- range $i, $p := .Params}}{{if $i}}, {{end}}{{valueType $p.Type}}{{end}}{{if not .HasParams}}{{end}} },
{{- end}}
{{- if or (needsRespType .) (isErrorOnly .)}}
[]extism.ValueType{extism.ValueTypePTR},
{{- else}}
[]extism.ValueType{ {{- range $i, $r := .Returns}}{{if $i}}, {{end}}{{valueType $r.Type}}{{end}}{{if not .HasReturns}}{{end}} },
{{- end}}
)
}
{{end}}
{{- if .NeedsWriteHelper}}
// {{.Service.Name | lower}}WriteResponse writes a JSON response to plugin memory.
func {{.Service.Name | lower}}WriteResponse(p *extism.CurrentPlugin, stack []uint64, resp any) {
respBytes, err := json.Marshal(resp)
if err != nil {
{{.Service.Name | lower}}WriteError(p, stack, err)
return
// formatDoc formats a documentation string for Go comments.
// It prefixes each line with "// " and trims trailing whitespace.
func formatDoc(doc string) string {
if doc == "" {
return ""
}
respPtr, err := p.WriteBytes(respBytes)
if err != nil {
stack[0] = 0
return
lines := strings.Split(strings.TrimSpace(doc), "\n")
var result []string
for _, line := range lines {
result = append(result, "// "+strings.TrimRight(line, " \t"))
}
stack[0] = respPtr
return strings.Join(result, "\n")
}
{{- end}}
{{- if .NeedsErrorHelper}}
// {{.Service.Name | lower}}WriteError writes an error response to plugin memory.
func {{.Service.Name | lower}}WriteError(p *extism.CurrentPlugin, stack []uint64, err error) {
errResp := struct {
Error string ` + "`" + `json:"error"` + "`" + `
}{Error: err.Error()}
respBytes, _ := json.Marshal(errResp)
respPtr, _ := p.WriteBytes(respBytes)
stack[0] = respPtr
}
{{- end}}
`

View File

@ -0,0 +1,139 @@
// Code generated by hostgen. DO NOT EDIT.
//
// This file contains client wrappers for the {{.Service.Name}} host service.
// It is intended for use in Navidrome plugins built with TinyGo.
package main
import (
{{- if .NeedsJSON}}
"encoding/json"
{{- end}}
{{- if .NeedsErrors}}
"errors"
{{- end}}
"github.com/extism/go-pdk"
)
{{- /* Generate wasmimport declarations for each method */ -}}
{{range .Service.Methods}}
// {{exportName .}} is the host function provided by Navidrome.
//
//go:wasmimport extism:host/user {{exportName .}}
func {{exportName .}}({{range $i, $p := .Params}}{{if $i}}, {{end}}{{wasmParamType $p}}{{end}}) {{wasmReturnType .}}
{{- end}}
{{- /* Generate response types for methods that need them */ -}}
{{range .Service.Methods}}
{{- if needsRespType .}}
// {{responseType .}} is the response type for {{$.Service.Name}}.{{.Name}}.
type {{responseType .}} struct {
{{- range .Returns}}
{{title .Name}} {{.Type}} `json:"{{.JSONName}},omitempty"`
{{- end}}
Error string `json:"error,omitempty"`
}
{{- end}}
{{- end}}
{{- /* Generate wrapper functions */ -}}
{{range .Service.Methods}}
// {{$.Service.Name}}{{.Name}} calls the {{exportName .}} host function.
{{- if .Doc}}
{{formatDoc .Doc}}
{{- end}}
func {{$.Service.Name}}{{.Name}}({{range $i, $p := .Params}}{{if $i}}, {{end}}{{$p.Name}} {{$p.Type}}{{end}}) {{wrapperReturnType . $.Service.Name}} {
{{- if needsRespType .}}
{{- /* Complex response - use JSON */}}
{{- range .Params}}
{{- if isString .Type}}
{{.Name}}Mem := pdk.AllocateString({{.Name}})
defer {{.Name}}Mem.Free()
{{- else if isBytes .Type}}
{{.Name}}Mem := pdk.AllocateBytes({{.Name}})
defer {{.Name}}Mem.Free()
{{- else if needsJSON .Type}}
{{.Name}}Bytes, err := json.Marshal({{.Name}})
if err != nil {
return nil, err
}
{{.Name}}Mem := pdk.AllocateBytes({{.Name}}Bytes)
defer {{.Name}}Mem.Free()
{{- end}}
{{- end}}
// Call the host function
responsePtr := {{exportName .}}({{range $i, $p := .Params}}{{if $i}}, {{end}}{{clientCallArg $p}}{{end}})
// Read the response from memory
responseMem := pdk.FindMemory(responsePtr)
responseBytes := responseMem.ReadBytes()
// Parse the response
var response {{responseType .}}
if err := json.Unmarshal(responseBytes, &response); err != nil {
return nil, err
}
if response.Error != "" {
return nil, errors.New(response.Error)
}
return &response, nil
{{- else if isErrorOnly .}}
{{- /* Error-only response - string result */}}
{{- range .Params}}
{{- if isString .Type}}
{{.Name}}Mem := pdk.AllocateString({{.Name}})
defer {{.Name}}Mem.Free()
{{- else if isBytes .Type}}
{{.Name}}Mem := pdk.AllocateBytes({{.Name}})
defer {{.Name}}Mem.Free()
{{- else if needsJSON .Type}}
{{.Name}}Bytes, err := json.Marshal({{.Name}})
if err != nil {
return err
}
{{.Name}}Mem := pdk.AllocateBytes({{.Name}}Bytes)
defer {{.Name}}Mem.Free()
{{- end}}
{{- end}}
// Call the host function
responsePtr := {{exportName .}}({{range $i, $p := .Params}}{{if $i}}, {{end}}{{clientCallArg $p}}{{end}})
// Read the response from memory
responseMem := pdk.FindMemory(responsePtr)
errStr := string(responseMem.ReadBytes())
if errStr != "" {
return errors.New(errStr)
}
return nil
{{- else}}
{{- /* Simple return types - direct stack values */}}
{{- range .Params}}
{{- if isString .Type}}
{{.Name}}Mem := pdk.AllocateString({{.Name}})
defer {{.Name}}Mem.Free()
{{- else if isBytes .Type}}
{{.Name}}Mem := pdk.AllocateBytes({{.Name}})
defer {{.Name}}Mem.Free()
{{- end}}
{{- end}}
// Call the host function
{{- if .HasReturns}}
result := {{exportName .}}({{range $i, $p := .Params}}{{if $i}}, {{end}}{{clientCallArg $p}}{{end}})
return {{decodeResult (index .Returns 0) "result"}}
{{- else}}
{{exportName .}}({{range $i, $p := .Params}}{{if $i}}, {{end}}{{clientCallArg $p}}{{end}})
{{- end}}
{{- end}}
}
{{- end}}

View File

@ -0,0 +1,161 @@
// Code generated by hostgen. DO NOT EDIT.
package {{.Package}}
import (
"context"
{{- if .NeedsJSON}}
"encoding/json"
{{- end}}
extism "github.com/extism/go-sdk"
)
{{- /* Generate request/response types only when needed */ -}}
{{range .Service.Methods}}
{{- if needsRequestType .}}
// {{requestType .}} is the request type for {{$.Service.Name}}.{{.Name}}.
type {{requestType .}} struct {
{{- range .Params}}
{{title .Name}} {{.Type}} `json:"{{.JSONName}}"`
{{- end}}
}
{{- end}}
{{- if needsRespType .}}
// {{responseType .}} is the response type for {{$.Service.Name}}.{{.Name}}.
type {{responseType .}} struct {
{{- range .Returns}}
{{title .Name}} {{.Type}} `json:"{{.JSONName}},omitempty"`
{{- end}}
Error string `json:"error,omitempty"`
}
{{- end}}
{{end}}
// Register{{.Service.Name}}HostFunctions registers {{.Service.Name}} service host functions.
// The returned host functions should be added to the plugin's configuration.
func Register{{.Service.Name}}HostFunctions(service {{.Service.Interface}}) []extism.HostFunction {
return []extism.HostFunction{
{{- range .Service.Methods}}
new{{$.Service.Name}}{{.Name}}HostFunction(service),
{{- end}}
}
}
{{range .Service.Methods}}
func new{{$.Service.Name}}{{.Name}}HostFunction(service {{$.Service.Interface}}) extism.HostFunction {
return extism.NewHostFunctionWithStack(
"{{exportName .}}",
func(ctx context.Context, p *extism.CurrentPlugin, stack []uint64) {
{{- if .HasParams}}
{{- if needsRequestType .}}
// Read JSON request from plugin memory
reqBytes, err := p.ReadBytes(stack[0])
if err != nil {
{{$.Service.Name | lower}}WriteError(p, stack, err)
return
}
var req {{requestType .}}
if err := json.Unmarshal(reqBytes, &req); err != nil {
{{$.Service.Name | lower}}WriteError(p, stack, err)
return
}
{{- else}}
// Read parameters from stack
{{- range $i, $p := .Params}}
{{readParam $p $i}}
{{- end}}
{{- end}}
{{- end}}
// Call the service method
{{- $m := .}}
{{- if .HasReturns}}
{{- if .HasError}}
{{range $i, $r := .Returns}}{{if $i}}, {{end}}{{lower $r.Name}}{{end}}, err := service.{{.Name}}(ctx{{range .Params}}, {{if needsRequestType $m}}req.{{title .Name}}{{else}}{{.Name}}{{end}}{{end}})
{{- else}}
{{range $i, $r := .Returns}}{{if $i}}, {{end}}{{lower $r.Name}}{{end}} := service.{{.Name}}(ctx{{range .Params}}, {{if needsRequestType $m}}req.{{title .Name}}{{else}}{{.Name}}{{end}}{{end}})
{{- end}}
{{- else if .HasError}}
err {{if hasErrFromRead .}}={{else}}:={{end}} service.{{.Name}}(ctx{{range .Params}}, {{if needsRequestType $m}}req.{{title .Name}}{{else}}{{.Name}}{{end}}{{end}})
{{- else}}
service.{{.Name}}(ctx{{range .Params}}, {{if needsRequestType $m}}req.{{title .Name}}{{else}}{{.Name}}{{end}}{{end}})
{{- end}}
{{- if .HasError}}
if err != nil {
{{- if isErrorOnly .}}
// Write error string to plugin memory
if ptr, err := p.WriteString(err.Error()); err == nil {
stack[0] = ptr
}
{{- else if needsRespType .}}
{{$.Service.Name | lower}}WriteError(p, stack, err)
{{- end}}
return
}
{{- end}}
{{- if isErrorOnly .}}
// Write empty string to indicate success
if ptr, err := p.WriteString(""); err == nil {
stack[0] = ptr
}
{{- else if needsRespType .}}
// Write JSON response to plugin memory
resp := {{responseType .}}{
{{- range .Returns}}
{{title .Name}}: {{lower .Name}},
{{- end}}
}
{{$.Service.Name | lower}}WriteResponse(p, stack, resp)
{{- else if .HasReturns}}
// Write return values to stack
{{- range $i, $r := .Returns}}
{{writeReturn $r $i (lower $r.Name)}}
{{- end}}
{{- end}}
},
{{- if needsRequestType $m}}
[]extism.ValueType{extism.ValueTypePTR},
{{- else}}
[]extism.ValueType{ {{- range $i, $p := .Params}}{{if $i}}, {{end}}{{valueType $p.Type}}{{end}}{{if not .HasParams}}{{end}} },
{{- end}}
{{- if or (needsRespType .) (isErrorOnly .)}}
[]extism.ValueType{extism.ValueTypePTR},
{{- else}}
[]extism.ValueType{ {{- range $i, $r := .Returns}}{{if $i}}, {{end}}{{valueType $r.Type}}{{end}}{{if not .HasReturns}}{{end}} },
{{- end}}
)
}
{{end}}
{{- if .NeedsWriteHelper}}
// {{.Service.Name | lower}}WriteResponse writes a JSON response to plugin memory.
func {{.Service.Name | lower}}WriteResponse(p *extism.CurrentPlugin, stack []uint64, resp any) {
respBytes, err := json.Marshal(resp)
if err != nil {
{{.Service.Name | lower}}WriteError(p, stack, err)
return
}
respPtr, err := p.WriteBytes(respBytes)
if err != nil {
stack[0] = 0
return
}
stack[0] = respPtr
}
{{- end}}
{{- if .NeedsErrorHelper}}
// {{.Service.Name | lower}}WriteError writes an error response to plugin memory.
func {{.Service.Name | lower}}WriteError(p *extism.CurrentPlugin, stack []uint64, err error) {
errResp := struct {
Error string `json:"error"`
}{Error: err.Error()}
respBytes, _ := json.Marshal(errResp)
respPtr, _ := p.WriteBytes(respBytes)
stack[0] = respPtr
}
{{- end}}

View File

@ -6,11 +6,13 @@
//
// Flags:
//
// -input Input directory containing Go source files with annotated interfaces
// -output Output directory for generated files (default: same as input)
// -package Output package name (default: inferred from output directory)
// -v Verbose output
// -dry-run Preview generated code without writing files
// -input Input directory containing Go source files with annotated interfaces
// -output Output directory for generated files (default: same as input)
// -package Output package name (default: inferred from output directory)
// -host-only Generate only host-side code (default: false)
// -plugin-only Generate only plugin/client-side code (default: false)
// -v Verbose output
// -dry-run Preview generated code without writing files
package main
import (
@ -19,20 +21,29 @@ import (
"go/format"
"os"
"path/filepath"
"strings"
"github.com/navidrome/navidrome/plugins/cmd/hostgen/internal"
)
func main() {
var (
inputDir = flag.String("input", ".", "Input directory containing Go source files")
outputDir = flag.String("output", "", "Output directory for generated files (default: same as input)")
pkgName = flag.String("package", "", "Output package name (default: inferred from output directory)")
verbose = flag.Bool("v", false, "Verbose output")
dryRun = flag.Bool("dry-run", false, "Preview generated code without writing files")
inputDir = flag.String("input", ".", "Input directory containing Go source files")
outputDir = flag.String("output", "", "Output directory for generated files (default: same as input)")
pkgName = flag.String("package", "", "Output package name (default: inferred from output directory)")
hostOnly = flag.Bool("host-only", false, "Generate only host-side code")
pluginOnly = flag.Bool("plugin-only", false, "Generate only plugin/client-side code")
verbose = flag.Bool("v", false, "Verbose output")
dryRun = flag.Bool("dry-run", false, "Preview generated code without writing files")
)
flag.Parse()
// Validate conflicting flags
if *hostOnly && *pluginOnly {
fmt.Fprintf(os.Stderr, "Error: -host-only and -plugin-only cannot be used together\n")
os.Exit(1)
}
if *outputDir == "" {
*outputDir = *inputDir
}
@ -54,10 +65,16 @@ func main() {
*pkgName = filepath.Base(absOutput)
}
// Determine what to generate
generateHost := !*pluginOnly
generateClient := !*hostOnly
if *verbose {
fmt.Printf("Input directory: %s\n", absInput)
fmt.Printf("Output directory: %s\n", absOutput)
fmt.Printf("Package name: %s\n", *pkgName)
fmt.Printf("Generate host code: %v\n", generateHost)
fmt.Printf("Generate client code: %v\n", generateClient)
}
// Parse source files
@ -83,34 +100,85 @@ func main() {
// Generate code for each service
for _, svc := range services {
code, err := internal.GenerateService(svc, *pkgName)
if err != nil {
fmt.Fprintf(os.Stderr, "Error generating code for %s: %v\n", svc.Name, err)
os.Exit(1)
// Generate host-side code
if generateHost {
if err := generateHostCode(svc, *pkgName, absOutput, *dryRun, *verbose); err != nil {
fmt.Fprintf(os.Stderr, "Error generating host code for %s: %v\n", svc.Name, err)
os.Exit(1)
}
}
// Format the generated code
formatted, err := format.Source(code)
if err != nil {
fmt.Fprintf(os.Stderr, "Error formatting generated code for %s: %v\n", svc.Name, err)
fmt.Fprintf(os.Stderr, "Raw code:\n%s\n", code)
os.Exit(1)
}
outputFile := filepath.Join(absOutput, svc.OutputFileName())
if *dryRun {
fmt.Printf("=== %s ===\n%s\n", outputFile, formatted)
continue
}
if err := os.WriteFile(outputFile, formatted, 0600); err != nil {
fmt.Fprintf(os.Stderr, "Error writing %s: %v\n", outputFile, err)
os.Exit(1)
}
if *verbose {
fmt.Printf("Generated %s\n", outputFile)
// Generate client-side code
if generateClient {
if err := generateClientCode(svc, absOutput, *dryRun, *verbose); err != nil {
fmt.Fprintf(os.Stderr, "Error generating client code for %s: %v\n", svc.Name, err)
os.Exit(1)
}
}
}
}
// generateHostCode generates host-side code for a service.
func generateHostCode(svc internal.Service, pkgName, outputDir string, dryRun, verbose bool) error {
code, err := internal.GenerateHost(svc, pkgName)
if err != nil {
return fmt.Errorf("generating code: %w", err)
}
formatted, err := format.Source(code)
if err != nil {
return fmt.Errorf("formatting code: %w\nRaw code:\n%s", err, code)
}
outputFile := filepath.Join(outputDir, svc.OutputFileName())
if dryRun {
fmt.Printf("=== %s ===\n%s\n", outputFile, formatted)
return nil
}
if err := os.WriteFile(outputFile, formatted, 0600); err != nil {
return fmt.Errorf("writing file: %w", err)
}
if verbose {
fmt.Printf("Generated host code: %s\n", outputFile)
}
return nil
}
// generateClientCode generates client-side code for a service.
func generateClientCode(svc internal.Service, outputDir string, dryRun, verbose bool) error {
code, err := internal.GenerateClientGo(svc)
if err != nil {
return fmt.Errorf("generating code: %w", err)
}
formatted, err := format.Source(code)
if err != nil {
return fmt.Errorf("formatting code: %w\nRaw code:\n%s", err, code)
}
// Client code goes in go/ subdirectory
clientDir := filepath.Join(outputDir, "go")
clientFile := filepath.Join(clientDir, "nd_host_"+strings.ToLower(svc.Name)+".go")
if dryRun {
fmt.Printf("=== %s ===\n%s\n", clientFile, formatted)
return nil
}
// Create go/ subdirectory if needed
if err := os.MkdirAll(clientDir, 0755); err != nil {
return fmt.Errorf("creating client directory: %w", err)
}
if err := os.WriteFile(clientFile, formatted, 0600); err != nil {
return fmt.Errorf("writing file: %w", err)
}
if verbose {
fmt.Printf("Generated client code: %s\n", clientFile)
}
return nil
}

View File

@ -0,0 +1,49 @@
// Code generated by hostgen. DO NOT EDIT.
//
// This file contains client wrappers for the Codec host service.
// It is intended for use in Navidrome plugins built with TinyGo.
package main
import (
"encoding/json"
"errors"
"github.com/extism/go-pdk"
)
// codec_encode is the host function provided by Navidrome.
//
//go:wasmimport extism:host/user codec_encode
func codec_encode(uint64) uint64
// CodecEncodeResponse is the response type for Codec.Encode.
type CodecEncodeResponse struct {
Result []byte `json:"result,omitempty"`
Error string `json:"error,omitempty"`
}
// CodecEncode calls the codec_encode host function.
func CodecEncode(data []byte) (*CodecEncodeResponse, error) {
dataMem := pdk.AllocateBytes(data)
defer dataMem.Free()
// Call the host function
responsePtr := codec_encode(dataMem.Offset())
// Read the response from memory
responseMem := pdk.FindMemory(responsePtr)
responseBytes := responseMem.ReadBytes()
// Parse the response
var response CodecEncodeResponse
if err := json.Unmarshal(responseBytes, &response); err != nil {
return nil, err
}
if response.Error != "" {
return nil, errors.New(response.Error)
}
return &response, nil
}

View File

@ -0,0 +1,25 @@
// Code generated by hostgen. DO NOT EDIT.
//
// This file contains client wrappers for the Counter host service.
// It is intended for use in Navidrome plugins built with TinyGo.
package main
import (
"github.com/extism/go-pdk"
)
// counter_count is the host function provided by Navidrome.
//
//go:wasmimport extism:host/user counter_count
func counter_count(uint64) int32
// CounterCount calls the counter_count host function.
func CounterCount(name string) int32 {
nameMem := pdk.AllocateString(name)
defer nameMem.Free()
// Call the host function
result := counter_count(nameMem.Offset())
return int32(result)
}

View File

@ -0,0 +1,49 @@
// Code generated by hostgen. DO NOT EDIT.
//
// This file contains client wrappers for the Echo host service.
// It is intended for use in Navidrome plugins built with TinyGo.
package main
import (
"encoding/json"
"errors"
"github.com/extism/go-pdk"
)
// echo_echo is the host function provided by Navidrome.
//
//go:wasmimport extism:host/user echo_echo
func echo_echo(uint64) uint64
// EchoEchoResponse is the response type for Echo.Echo.
type EchoEchoResponse struct {
Reply string `json:"reply,omitempty"`
Error string `json:"error,omitempty"`
}
// EchoEcho calls the echo_echo host function.
func EchoEcho(message string) (*EchoEchoResponse, error) {
messageMem := pdk.AllocateString(message)
defer messageMem.Free()
// Call the host function
responsePtr := echo_echo(messageMem.Offset())
// Read the response from memory
responseMem := pdk.FindMemory(responsePtr)
responseBytes := responseMem.ReadBytes()
// Parse the response
var response EchoEchoResponse
if err := json.Unmarshal(responseBytes, &response); err != nil {
return nil, err
}
if response.Error != "" {
return nil, errors.New(response.Error)
}
return &response, nil
}

View File

@ -0,0 +1,55 @@
// Code generated by hostgen. DO NOT EDIT.
//
// This file contains client wrappers for the List host service.
// It is intended for use in Navidrome plugins built with TinyGo.
package main
import (
"encoding/json"
"errors"
"github.com/extism/go-pdk"
)
// list_items is the host function provided by Navidrome.
//
//go:wasmimport extism:host/user list_items
func list_items(uint64, uint64) uint64
// ListItemsResponse is the response type for List.Items.
type ListItemsResponse struct {
Count int32 `json:"count,omitempty"`
Error string `json:"error,omitempty"`
}
// ListItems calls the list_items host function.
func ListItems(name string, filter Filter) (*ListItemsResponse, error) {
nameMem := pdk.AllocateString(name)
defer nameMem.Free()
filterBytes, err := json.Marshal(filter)
if err != nil {
return nil, err
}
filterMem := pdk.AllocateBytes(filterBytes)
defer filterMem.Free()
// Call the host function
responsePtr := list_items(nameMem.Offset(), filterMem.Offset())
// Read the response from memory
responseMem := pdk.FindMemory(responsePtr)
responseBytes := responseMem.ReadBytes()
// Parse the response
var response ListItemsResponse
if err := json.Unmarshal(responseBytes, &response); err != nil {
return nil, err
}
if response.Error != "" {
return nil, errors.New(response.Error)
}
return &response, nil
}

View File

@ -0,0 +1,47 @@
// Code generated by hostgen. DO NOT EDIT.
//
// This file contains client wrappers for the Math host service.
// It is intended for use in Navidrome plugins built with TinyGo.
package main
import (
"encoding/json"
"errors"
"github.com/extism/go-pdk"
)
// math_add is the host function provided by Navidrome.
//
//go:wasmimport extism:host/user math_add
func math_add(int32, int32) uint64
// MathAddResponse is the response type for Math.Add.
type MathAddResponse struct {
Result int32 `json:"result,omitempty"`
Error string `json:"error,omitempty"`
}
// MathAdd calls the math_add host function.
func MathAdd(a int32, b int32) (*MathAddResponse, error) {
// Call the host function
responsePtr := math_add(a, b)
// Read the response from memory
responseMem := pdk.FindMemory(responsePtr)
responseBytes := responseMem.ReadBytes()
// Parse the response
var response MathAddResponse
if err := json.Unmarshal(responseBytes, &response); err != nil {
return nil, err
}
if response.Error != "" {
return nil, errors.New(response.Error)
}
return &response, nil
}

View File

@ -0,0 +1,77 @@
// Code generated by hostgen. DO NOT EDIT.
//
// This file contains client wrappers for the Meta host service.
// It is intended for use in Navidrome plugins built with TinyGo.
package main
import (
"encoding/json"
"errors"
"github.com/extism/go-pdk"
)
// meta_get is the host function provided by Navidrome.
//
//go:wasmimport extism:host/user meta_get
func meta_get(uint64) uint64
// meta_set is the host function provided by Navidrome.
//
//go:wasmimport extism:host/user meta_set
func meta_set(uint64) uint64
// MetaGetResponse is the response type for Meta.Get.
type MetaGetResponse struct {
Value any `json:"value,omitempty"`
Error string `json:"error,omitempty"`
}
// MetaGet calls the meta_get host function.
func MetaGet(key string) (*MetaGetResponse, error) {
keyMem := pdk.AllocateString(key)
defer keyMem.Free()
// Call the host function
responsePtr := meta_get(keyMem.Offset())
// Read the response from memory
responseMem := pdk.FindMemory(responsePtr)
responseBytes := responseMem.ReadBytes()
// Parse the response
var response MetaGetResponse
if err := json.Unmarshal(responseBytes, &response); err != nil {
return nil, err
}
if response.Error != "" {
return nil, errors.New(response.Error)
}
return &response, nil
}
// MetaSet calls the meta_set host function.
func MetaSet(data map[string]any) error {
dataBytes, err := json.Marshal(data)
if err != nil {
return err
}
dataMem := pdk.AllocateBytes(dataBytes)
defer dataMem.Free()
// Call the host function
responsePtr := meta_set(dataMem.Offset())
// Read the response from memory
responseMem := pdk.FindMemory(responsePtr)
errStr := string(responseMem.ReadBytes())
if errStr != "" {
return errors.New(errStr)
}
return nil
}

View File

@ -0,0 +1,34 @@
// Code generated by hostgen. DO NOT EDIT.
//
// This file contains client wrappers for the Ping host service.
// It is intended for use in Navidrome plugins built with TinyGo.
package main
import (
"errors"
"github.com/extism/go-pdk"
)
// ping_ping is the host function provided by Navidrome.
//
//go:wasmimport extism:host/user ping_ping
func ping_ping() uint64
// PingPing calls the ping_ping host function.
func PingPing() error {
// Call the host function
responsePtr := ping_ping()
// Read the response from memory
responseMem := pdk.FindMemory(responsePtr)
errStr := string(responseMem.ReadBytes())
if errStr != "" {
return errors.New(errStr)
}
return nil
}

View File

@ -0,0 +1,50 @@
// Code generated by hostgen. DO NOT EDIT.
//
// This file contains client wrappers for the Search host service.
// It is intended for use in Navidrome plugins built with TinyGo.
package main
import (
"encoding/json"
"errors"
"github.com/extism/go-pdk"
)
// search_find is the host function provided by Navidrome.
//
//go:wasmimport extism:host/user search_find
func search_find(uint64) uint64
// SearchFindResponse is the response type for Search.Find.
type SearchFindResponse struct {
Results []Result `json:"results,omitempty"`
Total int32 `json:"total,omitempty"`
Error string `json:"error,omitempty"`
}
// SearchFind calls the search_find host function.
func SearchFind(query string) (*SearchFindResponse, error) {
queryMem := pdk.AllocateString(query)
defer queryMem.Free()
// Call the host function
responsePtr := search_find(queryMem.Offset())
// Read the response from memory
responseMem := pdk.FindMemory(responsePtr)
responseBytes := responseMem.ReadBytes()
// Parse the response
var response SearchFindResponse
if err := json.Unmarshal(responseBytes, &response); err != nil {
return nil, err
}
if response.Error != "" {
return nil, errors.New(response.Error)
}
return &response, nil
}

View File

@ -0,0 +1,53 @@
// Code generated by hostgen. DO NOT EDIT.
//
// This file contains client wrappers for the Store host service.
// It is intended for use in Navidrome plugins built with TinyGo.
package main
import (
"encoding/json"
"errors"
"github.com/extism/go-pdk"
)
// store_save is the host function provided by Navidrome.
//
//go:wasmimport extism:host/user store_save
func store_save(uint64) uint64
// StoreSaveResponse is the response type for Store.Save.
type StoreSaveResponse struct {
Id string `json:"id,omitempty"`
Error string `json:"error,omitempty"`
}
// StoreSave calls the store_save host function.
func StoreSave(item Item) (*StoreSaveResponse, error) {
itemBytes, err := json.Marshal(item)
if err != nil {
return nil, err
}
itemMem := pdk.AllocateBytes(itemBytes)
defer itemMem.Free()
// Call the host function
responsePtr := store_save(itemMem.Offset())
// Read the response from memory
responseMem := pdk.FindMemory(responsePtr)
responseBytes := responseMem.ReadBytes()
// Parse the response
var response StoreSaveResponse
if err := json.Unmarshal(responseBytes, &response); err != nil {
return nil, err
}
if response.Error != "" {
return nil, errors.New(response.Error)
}
return &response, nil
}

View File

@ -0,0 +1,59 @@
// Code generated by hostgen. DO NOT EDIT.
//
// This file contains client wrappers for the Users host service.
// It is intended for use in Navidrome plugins built with TinyGo.
package main
import (
"encoding/json"
"errors"
"github.com/extism/go-pdk"
)
// users_get is the host function provided by Navidrome.
//
//go:wasmimport extism:host/user users_get
func users_get(uint64, uint64) uint64
// UsersGetResponse is the response type for Users.Get.
type UsersGetResponse struct {
Result *User `json:"result,omitempty"`
Error string `json:"error,omitempty"`
}
// UsersGet calls the users_get host function.
func UsersGet(id *string, filter *User) (*UsersGetResponse, error) {
idBytes, err := json.Marshal(id)
if err != nil {
return nil, err
}
idMem := pdk.AllocateBytes(idBytes)
defer idMem.Free()
filterBytes, err := json.Marshal(filter)
if err != nil {
return nil, err
}
filterMem := pdk.AllocateBytes(filterBytes)
defer filterMem.Free()
// Call the host function
responsePtr := users_get(idMem.Offset(), filterMem.Offset())
// Read the response from memory
responseMem := pdk.FindMemory(responsePtr)
responseBytes := responseMem.ReadBytes()
// Parse the response
var response UsersGetResponse
if err := json.Unmarshal(responseBytes, &response); err != nil {
return nil, err
}
if response.Error != "" {
return nil, errors.New(response.Error)
}
return &response, nil
}

View File

@ -0,0 +1,53 @@
// Code generated by hostgen. DO NOT EDIT.
//
// This file contains client wrappers for the SubsonicAPI host service.
// It is intended for use in Navidrome plugins built with TinyGo.
package main
import (
"encoding/json"
"errors"
"github.com/extism/go-pdk"
)
// subsonicapi_call is the host function provided by Navidrome.
//
//go:wasmimport extism:host/user subsonicapi_call
func subsonicapi_call(uint64) uint64
// SubsonicAPICallResponse is the response type for SubsonicAPI.Call.
type SubsonicAPICallResponse struct {
ResponseJSON string `json:"responseJSON,omitempty"`
Error string `json:"error,omitempty"`
}
// SubsonicAPICall calls the subsonicapi_call host function.
// Call executes a Subsonic API request and returns the JSON response.
//
// The uri parameter should be the Subsonic API path without the server prefix,
// e.g., "getAlbumList2?type=random&size=10". The response is returned as raw JSON.
func SubsonicAPICall(uri string) (*SubsonicAPICallResponse, error) {
uriMem := pdk.AllocateString(uri)
defer uriMem.Free()
// Call the host function
responsePtr := subsonicapi_call(uriMem.Offset())
// Read the response from memory
responseMem := pdk.FindMemory(responsePtr)
responseBytes := responseMem.ReadBytes()
// Parse the response
var response SubsonicAPICallResponse
if err := json.Unmarshal(responseBytes, &response); err != nil {
return nil, err
}
if response.Error != "" {
return nil, errors.New(response.Error)
}
return &response, nil
}

View File

@ -11,7 +11,7 @@ all: $(PLUGINS:%=%.wasm)
clean:
rm -f $(PLUGINS:%=%.wasm)
%.wasm: %/main.go %/go.mod
%.wasm: %/*.go %/go.mod
ifdef TINYGO
cd $* && tinygo build -target wasip1 -buildmode=c-shared -o ../$@ .
else

View File

@ -1,3 +1,8 @@
// Code generated by hostgen. DO NOT EDIT.
//
// This file contains client wrappers for the SubsonicAPI host service.
// It is intended for use in Navidrome plugins built with TinyGo.
package main
import (
@ -7,26 +12,28 @@ import (
"github.com/extism/go-pdk"
)
// subsonicapiCall is the host function provided by Navidrome to call the Subsonic API.
// It takes a URI string and returns a JSON response.
// subsonicapi_call is the host function provided by Navidrome.
//
//go:wasmimport extism:host/user subsonicapi_call
func subsonicapiCall(uri uint64) uint64
func subsonicapi_call(uint64) uint64
// SubsonicAPICallResponse matches the host response format.
// SubsonicAPICallResponse is the response type for SubsonicAPI.Call.
type SubsonicAPICallResponse struct {
ResponseJSON string `json:"responseJSON,omitempty"`
Error string `json:"error,omitempty"`
}
// SubsonicAPICall is a wrapper around the host subsonicapi_call function.
// SubsonicAPICall calls the subsonicapi_call host function.
// Call executes a Subsonic API request and returns the JSON response.
//
// The uri parameter should be the Subsonic API path without the server prefix,
// e.g., "getAlbumList2?type=random&size=10". The response is returned as raw JSON.
func SubsonicAPICall(uri string) (*SubsonicAPICallResponse, error) {
// Allocate memory for the URI string
mem := pdk.AllocateString(uri)
defer mem.Free()
uriMem := pdk.AllocateString(uri)
defer uriMem.Free()
// Call the host function
responsePtr := subsonicapiCall(mem.Offset())
responsePtr := subsonicapi_call(uriMem.Offset())
// Read the response from memory
responseMem := pdk.FindMemory(responsePtr)