# hostgen A code generator for Navidrome's plugin host functions. It reads Go interface definitions with special annotations and generates Extism host function wrappers. ## Usage ```bash hostgen -input -output -package [-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` | | `-host-only` | Generate only host-side wrapper code | `false` | | `-plugin-only` | Generate only plugin/client-side wrapper code | `false` | | `-go` | Generate Go client wrappers | `true`* | | `-python` | Generate Python client wrappers | `false` | | `-rust` | Generate Rust client wrappers | `false` | \* `-go` is enabled by default when neither `-python` nor `-rust` is specified. Use combinations like `-go -python -rust` to generate multiple languages. By default, both host and Go plugin code are generated. Use `-host-only` or `-plugin-only` to generate only one type. Use `-python` to generate Python wrappers and `-rust` to generate Rust wrappers. ### Example ```bash go run ./plugins/cmd/hostgen \ -input ./plugins/host \ -output ./plugins/host \ -package host ``` Or via `go generate` (recommended): ```go //go:generate go run ../cmd/hostgen -input . -output . -package host package host ``` ## Annotations ### `//nd:hostservice` Marks an interface as a host service that will have wrappers generated. ```go //nd:hostservice name= permission= type MyService interface { ... } ``` | Parameter | Description | Required | |--------------|-----------------------------------------------------------------|----------| | `name` | Service name used in generated type names and function prefixes | Yes | | `permission` | Permission required by plugins to use this service | Yes | ### `//nd:hostfunc` Marks a method within a host service interface for export to plugins. ```go //nd:hostfunc [name=] MethodName(ctx context.Context, ...) (result Type, err error) ``` | Parameter | Description | Required | |-----------|-------------------------------------------------------------------------|----------| | `name` | Custom export name (default: `_` in lowercase) | No | ## Input Format Host service interfaces must follow these conventions: 1. **First parameter must be `context.Context`** - Required for all methods 2. **Last return value should be `error`** - For proper error handling 3. **Annotations must be on consecutive lines** - No blank comment lines between doc and annotation ### Example Interface ```go package host import "context" // SubsonicAPIService provides access to Navidrome's Subsonic API. // This documentation becomes part of the generated code. //nd:hostservice name=SubsonicAPI permission=subsonicapi type SubsonicAPIService interface { // Call executes a Subsonic API request and returns the response. //nd:hostfunc Call(ctx context.Context, uri string) (response string, err error) } ``` ## Generated Output For each annotated interface, hostgen generates: ### Request/Response Types ```go // SubsonicAPICallRequest is the request type for SubsonicAPI.Call. type SubsonicAPICallRequest struct { Uri string `json:"uri"` } // SubsonicAPICallResponse is the response type for SubsonicAPI.Call. type SubsonicAPICallResponse struct { Response string `json:"response,omitempty"` Error string `json:"error,omitempty"` } ``` ### Registration Function ```go // RegisterSubsonicAPIHostFunctions registers SubsonicAPI service host functions. func RegisterSubsonicAPIHostFunctions(service SubsonicAPIService) []extism.HostFunction { return []extism.HostFunction{ newSubsonicAPICallHostFunction(service), } } ``` ### Host Function Wrappers Each method gets a wrapper that: 1. Reads JSON request from plugin memory 2. Unmarshals to the request type 3. Calls the service method 4. Marshals the response 5. Writes JSON response to plugin memory ## Supported Types hostgen supports these Go types in method signatures: | Type | JSON Representation | |-------------------------------|------------------------------------------| | `string`, `int`, `bool`, etc. | Native JSON types | | `[]T` (slices) | JSON arrays | | `map[K]V` (maps) | JSON objects | | `*T` (pointers) | Nullable fields | | `interface{}` / `any` | Converts to `any` | | Custom structs | JSON objects (must be JSON-serializable) | ### Multiple Return Values Methods can return multiple values (plus error): ```go //nd:hostfunc Search(ctx context.Context, query string) (results []string, total int, hasMore bool, err error) ``` Generates: ```go type ServiceSearchResponse struct { Results []string `json:"results,omitempty"` Total int `json:"total,omitempty"` HasMore bool `json:"hasMore,omitempty"` Error string `json:"error,omitempty"` } ``` ## Output Files ### Host Code (Navidrome-side) Generated files are named `_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`) - Request/response struct types - Registration function - Host function wrappers - Helper functions (`writeResponse`, `writeErrorResponse`) ### Go Client Library (Go/TinyGo WASM) Generated files are named `nd_host_.go` (lowercase) and placed in the `go/` subdirectory of the output directory. The `go/` directory is a complete Go module (`github.com/navidrome/navidrome/plugins/host/go`) with package name `ndhost`, intended for import by Navidrome plugins built with TinyGo. The generator also creates: - `doc.go` - Package documentation listing all available services - `go.mod` - Go module file with required dependencies Each service 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 and any struct definitions from the service - Wrapper functions that handle memory allocation and JSON parsing #### Using the Go SDK Import the SDK in your plugin: ```go import ndhost "github.com/navidrome/navidrome/plugins/host/go" // Use host services with the ndhost prefix result, err := ndhost.CacheGetString("my-key") scheduleID, err := ndhost.SchedulerScheduleOneTime(60, "payload", "") ``` Add to your `go.mod`: ``` require github.com/navidrome/navidrome/plugins/host/go v0.0.0 replace github.com/navidrome/navidrome/plugins/host/go => ../../host/go ``` See [plugins/host/go/README.md](../../host/go/README.md) for detailed documentation. ### Example Output Structure ``` output/ ├── subsonicapi_gen.go # Host-side code (for Navidrome) ├── go/ │ ├── doc.go # Package documentation │ ├── go.mod # Go module file │ └── nd_host_subsonicapi.go # Plugin-side code (for TinyGo plugins) ├── python/ │ └── nd_host_subsonicapi.py # Plugin-side code (for Python plugins) └── rust/ └── nd_host_subsonicapi.rs # Plugin-side code (for Rust plugins) ``` ### Python Client Code (extism-py WASM) Generated files are named `nd_host_.py` (lowercase) and placed in the `python/` subdirectory of the output directory. These files are intended for use in Navidrome plugins built with extism-py. Each file includes: - `# Code generated by hostgen. DO NOT EDIT.` header - Required imports (`dataclasses`, `typing`, `extism`, `json`) - `HostFunctionError` exception class for error handling - `@extism.import_fn` declarations for raw host functions - `@dataclass` types for methods with multiple return values - Wrapper functions with type hints, docstrings, and snake_case names #### Python Type Mapping | Go Type | Python Type | |-------------------------|-------------| | `string` | `str` | | `int`, `int32`, `int64` | `int` | | `float32`, `float64` | `float` | | `bool` | `bool` | | `[]byte` | `bytes` | | Unknown | `Any` | #### Python Function Naming Functions follow PEP 8 snake_case convention: | Go Method | Python Function | |-------------------------------|----------------------------------| | `SubsonicAPI.Call` | `subsonicapi_call()` | | `Scheduler.ScheduleRecurring` | `scheduler_schedule_recurring()` | | `Cache.GetString` | `cache_get_string()` | #### Multi-Value Returns Methods with multiple return values use dataclasses: ```python @dataclass class CacheGetStringResult: value: str exists: bool def cache_get_string(key: str) -> CacheGetStringResult: ... ``` #### Python Plugin Usage > **Important:** Due to a limitation in extism-py, you cannot directly import the generated Python wrappers. > The `@extism.import_fn` decorators are only detected when defined in the plugin's main `__init__.py` file. > Generated Python files serve as **reference/template code** - copy the needed functions into your plugin. Example of copying the generated wrapper into your plugin's `__init__.py`: ```python import extism import json # Copy host function declarations from generated files into your __init__.py @extism.import_fn("extism:host/user", "subsonicapi_call") def _host_subsonicapi_call(input_ptr: extism.JsonI64) -> extism.JsonI64: pass def subsonicapi_call(endpoint: str) -> str: """Call the SubsonicAPI with the given endpoint.""" result = _host_subsonicapi_call(endpoint) return result # Now use it in your plugin @extism.plugin_fn def my_plugin_function(): try: response = subsonicapi_call("getAlbumList2?type=random&size=10") data = json.loads(response) except Exception as e: extism.log(extism.LogLevel.Error, f"API error: {e}") ``` ### Rust Client Code (extism-pdk WASM) Generated files are named `nd_host_.rs` (lowercase) and placed in the `rust/` subdirectory of the output directory. These files are intended for use in Navidrome plugins built with `extism-pdk`. Each file includes: - `// Code generated by hostgen. DO NOT EDIT.` header - Required imports (`extism_pdk::*`, `serde`) - Request/response struct types with `#[derive(Serialize, Deserialize)]` - `#[host_fn]` extern blocks for raw host function imports - Public wrapper functions with `Result` returns and snake_case names #### Rust Type Mapping | Go Type | Rust Type | Notes | |-------------------------------|-------------------------------|------------------------------| | `string` | `String` / `&str` | `&str` for params | | `int`, `int32` | `i32` | | | `int64` | `i64` | | | `float32` | `f32` | | | `float64` | `f64` | | | `bool` | `bool` | | | `[]byte` | `Vec` | | | `[]T` | `Vec` | | | `map[K]V` | `HashMap` | From `std::collections` | | `*T` | `Option` | | | `interface{}` / `any` | `serde_json::Value` | | #### Rust Function Naming Functions follow Rust snake_case convention: | Go Method | Rust Function | |-------------------------------|----------------------------------| | `SubsonicAPI.Call` | `subsonicapi_call()` | | `Scheduler.ScheduleRecurring` | `scheduler_schedule_recurring()` | | `Cache.GetString` | `cache_get_string()` | #### Rust Plugin Usage The generated Rust wrappers form a library crate (`nd-host`) that plugins can depend on. Add the dependency to your plugin's `Cargo.toml`: ```toml [dependencies] extism-pdk = "1.2" nd-host = { path = "../../host/rust" } ``` Then import and use the services: ```rust use nd_host::{cache, scheduler, artwork}; #[plugin_fn] pub fn my_callback(input: String) -> FnResult { // Use cache service cache::cache_set_string("key", "value", 3600)?; let value = cache::cache_get_string("key")?; // Schedule a task scheduler::scheduler_schedule_one_time(60, "payload", "task-id")?; // Get artwork URL let url = artwork::artwork_get_track_url("track-id", 300)?; Ok("done") } ``` See [discord-rich-presence-rs](../examples/discord-rich-presence-rs/) for a complete example using all Rust host wrappers. ## Troubleshooting ### Annotations Not Detected Ensure annotations are on consecutive lines with no blank `//` lines: ```go // ✅ Correct // Documentation for the service. //nd:hostservice name=Test permission=test // ❌ Wrong - blank comment line breaks detection // Documentation for the service. // //nd:hostservice name=Test permission=test ``` ### Methods Not Exported Methods without `//nd:hostfunc` annotation are skipped. Ensure the annotation is directly above the method: ```go // ✅ Correct // Method documentation. //nd:hostfunc MyMethod(ctx context.Context) error // ❌ Wrong - annotation not directly above method //nd:hostfunc MyMethod(ctx context.Context) error ``` ### Generated Files Skipped Files ending in `_gen.go` are automatically skipped during parsing to avoid processing previously generated code. ## Development Run tests: ```bash go test -v ./plugins/cmd/hostgen/... ``` The test suite includes: - CLI integration tests - Complex type handling (structs, slices, maps, pointers) - Multiple return value scenarios - Error cases and edge conditions