builder.go

  1package wazero
  2
  3import (
  4	"context"
  5
  6	"github.com/tetratelabs/wazero/api"
  7	"github.com/tetratelabs/wazero/internal/wasm"
  8)
  9
 10// HostFunctionBuilder defines a host function (in Go), so that a
 11// WebAssembly binary (e.g. %.wasm file) can import and use it.
 12//
 13// Here's an example of an addition function:
 14//
 15//	hostModuleBuilder.NewFunctionBuilder().
 16//		WithFunc(func(cxt context.Context, x, y uint32) uint32 {
 17//			return x + y
 18//		}).
 19//		Export("add")
 20//
 21// # Memory
 22//
 23// All host functions act on the importing api.Module, including any memory
 24// exported in its binary (%.wasm file). If you are reading or writing memory,
 25// it is sand-boxed Wasm memory defined by the guest.
 26//
 27// Below, `m` is the importing module, defined in Wasm. `fn` is a host function
 28// added via Export. This means that `x` was read from memory defined in Wasm,
 29// not arbitrary memory in the process.
 30//
 31//	fn := func(ctx context.Context, m api.Module, offset uint32) uint32 {
 32//		x, _ := m.Memory().ReadUint32Le(ctx, offset)
 33//		return x
 34//	}
 35//
 36// # Notes
 37//
 38//   - This is an interface for decoupling, not third-party implementations.
 39//     All implementations are in wazero.
 40type HostFunctionBuilder interface {
 41	// WithGoFunction is an advanced feature for those who need higher
 42	// performance than WithFunc at the cost of more complexity.
 43	//
 44	// Here's an example addition function:
 45	//
 46	//	builder.WithGoFunction(api.GoFunc(func(ctx context.Context, stack []uint64) {
 47	//		x, y := api.DecodeI32(stack[0]), api.DecodeI32(stack[1])
 48	//		sum := x + y
 49	//		stack[0] = api.EncodeI32(sum)
 50	//	}), []api.ValueType{api.ValueTypeI32, api.ValueTypeI32}, []api.ValueType{api.ValueTypeI32})
 51	//
 52	// As you can see above, defining in this way implies knowledge of which
 53	// WebAssembly api.ValueType is appropriate for each parameter and result.
 54	//
 55	// See WithGoModuleFunction if you also need to access the calling module.
 56	WithGoFunction(fn api.GoFunction, params, results []api.ValueType) HostFunctionBuilder
 57
 58	// WithGoModuleFunction is an advanced feature for those who need higher
 59	// performance than WithFunc at the cost of more complexity.
 60	//
 61	// Here's an example addition function that loads operands from memory:
 62	//
 63	//	builder.WithGoModuleFunction(api.GoModuleFunc(func(ctx context.Context, m api.Module, stack []uint64) {
 64	//		mem := m.Memory()
 65	//		offset := api.DecodeU32(stack[0])
 66	//
 67	//		x, _ := mem.ReadUint32Le(ctx, offset)
 68	//		y, _ := mem.ReadUint32Le(ctx, offset + 4) // 32 bits == 4 bytes!
 69	//		sum := x + y
 70	//
 71	//		stack[0] = api.EncodeU32(sum)
 72	//	}), []api.ValueType{api.ValueTypeI32}, []api.ValueType{api.ValueTypeI32})
 73	//
 74	// As you can see above, defining in this way implies knowledge of which
 75	// WebAssembly api.ValueType is appropriate for each parameter and result.
 76	//
 77	// See WithGoFunction if you don't need access to the calling module.
 78	WithGoModuleFunction(fn api.GoModuleFunction, params, results []api.ValueType) HostFunctionBuilder
 79
 80	// WithFunc uses reflect.Value to map a go `func` to a WebAssembly
 81	// compatible Signature. An input that isn't a `func` will fail to
 82	// instantiate.
 83	//
 84	// Here's an example of an addition function:
 85	//
 86	//	builder.WithFunc(func(cxt context.Context, x, y uint32) uint32 {
 87	//		return x + y
 88	//	})
 89	//
 90	// # Defining a function
 91	//
 92	// Except for the context.Context and optional api.Module, all parameters
 93	// or result types must map to WebAssembly numeric value types. This means
 94	// uint32, int32, uint64, int64, float32 or float64.
 95	//
 96	// api.Module may be specified as the second parameter, usually to access
 97	// memory. This is important because there are only numeric types in Wasm.
 98	// The only way to share other data is via writing memory and sharing
 99	// offsets.
100	//
101	//	builder.WithFunc(func(ctx context.Context, m api.Module, offset uint32) uint32 {
102	//		mem := m.Memory()
103	//		x, _ := mem.ReadUint32Le(ctx, offset)
104	//		y, _ := mem.ReadUint32Le(ctx, offset + 4) // 32 bits == 4 bytes!
105	//		return x + y
106	//	})
107	//
108	// This example propagates context properly when calling other functions
109	// exported in the api.Module:
110	//
111	//	builder.WithFunc(func(ctx context.Context, m api.Module, offset, byteCount uint32) uint32 {
112	//		fn = m.ExportedFunction("__read")
113	//		results, err := fn(ctx, offset, byteCount)
114	//	--snip--
115	WithFunc(interface{}) HostFunctionBuilder
116
117	// WithName defines the optional module-local name of this function, e.g.
118	// "random_get"
119	//
120	// Note: This is not required to match the Export name.
121	WithName(name string) HostFunctionBuilder
122
123	// WithParameterNames defines optional parameter names of the function
124	// signature, e.x. "buf", "buf_len"
125	//
126	// Note: When defined, names must be provided for all parameters.
127	WithParameterNames(names ...string) HostFunctionBuilder
128
129	// WithResultNames defines optional result names of the function
130	// signature, e.x. "errno"
131	//
132	// Note: When defined, names must be provided for all results.
133	WithResultNames(names ...string) HostFunctionBuilder
134
135	// Export exports this to the HostModuleBuilder as the given name, e.g.
136	// "random_get"
137	Export(name string) HostModuleBuilder
138}
139
140// HostModuleBuilder is a way to define host functions (in Go), so that a
141// WebAssembly binary (e.g. %.wasm file) can import and use them.
142//
143// Specifically, this implements the host side of an Application Binary
144// Interface (ABI) like WASI or AssemblyScript.
145//
146// For example, this defines and instantiates a module named "env" with one
147// function:
148//
149//	ctx := context.Background()
150//	r := wazero.NewRuntime(ctx)
151//	defer r.Close(ctx) // This closes everything this Runtime created.
152//
153//	hello := func() {
154//		println("hello!")
155//	}
156//	env, _ := r.NewHostModuleBuilder("env").
157//		NewFunctionBuilder().WithFunc(hello).Export("hello").
158//		Instantiate(ctx)
159//
160// If the same module may be instantiated multiple times, it is more efficient
161// to separate steps. Here's an example:
162//
163//	compiled, _ := r.NewHostModuleBuilder("env").
164//		NewFunctionBuilder().WithFunc(getRandomString).Export("get_random_string").
165//		Compile(ctx)
166//
167//	env1, _ := r.InstantiateModule(ctx, compiled, wazero.NewModuleConfig().WithName("env.1"))
168//	env2, _ := r.InstantiateModule(ctx, compiled, wazero.NewModuleConfig().WithName("env.2"))
169//
170// See HostFunctionBuilder for valid host function signatures and other details.
171//
172// # Notes
173//
174//   - This is an interface for decoupling, not third-party implementations.
175//     All implementations are in wazero.
176//   - HostModuleBuilder is mutable: each method returns the same instance for
177//     chaining.
178//   - methods do not return errors, to allow chaining. Any validation errors
179//     are deferred until Compile.
180//   - Functions are indexed in order of calls to NewFunctionBuilder as
181//     insertion ordering is needed by ABI such as Emscripten (invoke_*).
182//   - The semantics of host functions assumes the existence of an "importing module" because, for example, the host function needs access to
183//     the memory of the importing module. Therefore, direct use of ExportedFunction is forbidden for host modules.
184//     Practically speaking, it is usually meaningless to directly call a host function from Go code as it is already somewhere in Go code.
185type HostModuleBuilder interface {
186	// Note: until golang/go#5860, we can't use example tests to embed code in interface godocs.
187
188	// NewFunctionBuilder begins the definition of a host function.
189	NewFunctionBuilder() HostFunctionBuilder
190
191	// Compile returns a CompiledModule that can be instantiated by Runtime.
192	Compile(context.Context) (CompiledModule, error)
193
194	// Instantiate is a convenience that calls Compile, then Runtime.InstantiateModule.
195	// This can fail for reasons documented on Runtime.InstantiateModule.
196	//
197	// Here's an example:
198	//
199	//	ctx := context.Background()
200	//	r := wazero.NewRuntime(ctx)
201	//	defer r.Close(ctx) // This closes everything this Runtime created.
202	//
203	//	hello := func() {
204	//		println("hello!")
205	//	}
206	//	env, _ := r.NewHostModuleBuilder("env").
207	//		NewFunctionBuilder().WithFunc(hello).Export("hello").
208	//		Instantiate(ctx)
209	//
210	// # Notes
211	//
212	//   - Closing the Runtime has the same effect as closing the result.
213	//   - Fields in the builder are copied during instantiation: Later changes do not affect the instantiated result.
214	//   - To avoid using configuration defaults, use Compile instead.
215	Instantiate(context.Context) (api.Module, error)
216}
217
218// hostModuleBuilder implements HostModuleBuilder
219type hostModuleBuilder struct {
220	r              *runtime
221	moduleName     string
222	exportNames    []string
223	nameToHostFunc map[string]*wasm.HostFunc
224}
225
226// NewHostModuleBuilder implements Runtime.NewHostModuleBuilder
227func (r *runtime) NewHostModuleBuilder(moduleName string) HostModuleBuilder {
228	return &hostModuleBuilder{
229		r:              r,
230		moduleName:     moduleName,
231		nameToHostFunc: map[string]*wasm.HostFunc{},
232	}
233}
234
235// hostFunctionBuilder implements HostFunctionBuilder
236type hostFunctionBuilder struct {
237	b           *hostModuleBuilder
238	fn          interface{}
239	name        string
240	paramNames  []string
241	resultNames []string
242}
243
244// WithGoFunction implements HostFunctionBuilder.WithGoFunction
245func (h *hostFunctionBuilder) WithGoFunction(fn api.GoFunction, params, results []api.ValueType) HostFunctionBuilder {
246	h.fn = &wasm.HostFunc{ParamTypes: params, ResultTypes: results, Code: wasm.Code{GoFunc: fn}}
247	return h
248}
249
250// WithGoModuleFunction implements HostFunctionBuilder.WithGoModuleFunction
251func (h *hostFunctionBuilder) WithGoModuleFunction(fn api.GoModuleFunction, params, results []api.ValueType) HostFunctionBuilder {
252	h.fn = &wasm.HostFunc{ParamTypes: params, ResultTypes: results, Code: wasm.Code{GoFunc: fn}}
253	return h
254}
255
256// WithFunc implements HostFunctionBuilder.WithFunc
257func (h *hostFunctionBuilder) WithFunc(fn interface{}) HostFunctionBuilder {
258	h.fn = fn
259	return h
260}
261
262// WithName implements HostFunctionBuilder.WithName
263func (h *hostFunctionBuilder) WithName(name string) HostFunctionBuilder {
264	h.name = name
265	return h
266}
267
268// WithParameterNames implements HostFunctionBuilder.WithParameterNames
269func (h *hostFunctionBuilder) WithParameterNames(names ...string) HostFunctionBuilder {
270	h.paramNames = names
271	return h
272}
273
274// WithResultNames implements HostFunctionBuilder.WithResultNames
275func (h *hostFunctionBuilder) WithResultNames(names ...string) HostFunctionBuilder {
276	h.resultNames = names
277	return h
278}
279
280// Export implements HostFunctionBuilder.Export
281func (h *hostFunctionBuilder) Export(exportName string) HostModuleBuilder {
282	var hostFn *wasm.HostFunc
283	if fn, ok := h.fn.(*wasm.HostFunc); ok {
284		hostFn = fn
285	} else {
286		hostFn = &wasm.HostFunc{Code: wasm.Code{GoFunc: h.fn}}
287	}
288
289	// Assign any names from the builder
290	hostFn.ExportName = exportName
291	if h.name != "" {
292		hostFn.Name = h.name
293	}
294	if len(h.paramNames) != 0 {
295		hostFn.ParamNames = h.paramNames
296	}
297	if len(h.resultNames) != 0 {
298		hostFn.ResultNames = h.resultNames
299	}
300
301	h.b.ExportHostFunc(hostFn)
302	return h.b
303}
304
305// ExportHostFunc implements wasm.HostFuncExporter
306func (b *hostModuleBuilder) ExportHostFunc(fn *wasm.HostFunc) {
307	if _, ok := b.nameToHostFunc[fn.ExportName]; !ok { // add a new name
308		b.exportNames = append(b.exportNames, fn.ExportName)
309	}
310	b.nameToHostFunc[fn.ExportName] = fn
311}
312
313// NewFunctionBuilder implements HostModuleBuilder.NewFunctionBuilder
314func (b *hostModuleBuilder) NewFunctionBuilder() HostFunctionBuilder {
315	return &hostFunctionBuilder{b: b}
316}
317
318// Compile implements HostModuleBuilder.Compile
319func (b *hostModuleBuilder) Compile(ctx context.Context) (CompiledModule, error) {
320	module, err := wasm.NewHostModule(b.moduleName, b.exportNames, b.nameToHostFunc, b.r.enabledFeatures)
321	if err != nil {
322		return nil, err
323	} else if err = module.Validate(b.r.enabledFeatures); err != nil {
324		return nil, err
325	}
326
327	c := &compiledModule{module: module, compiledEngine: b.r.store.Engine}
328	listeners, err := buildFunctionListeners(ctx, module)
329	if err != nil {
330		return nil, err
331	}
332
333	if err = b.r.store.Engine.CompileModule(ctx, module, listeners, false); err != nil {
334		return nil, err
335	}
336
337	// typeIDs are static and compile-time known.
338	typeIDs, err := b.r.store.GetFunctionTypeIDs(module.TypeSection)
339	if err != nil {
340		return nil, err
341	}
342	c.typeIDs = typeIDs
343
344	return c, nil
345}
346
347// hostModuleInstance is a wrapper around api.Module that prevents calling ExportedFunction.
348type hostModuleInstance struct{ api.Module }
349
350// ExportedFunction implements api.Module ExportedFunction.
351func (h hostModuleInstance) ExportedFunction(name string) api.Function {
352	panic("calling ExportedFunction is forbidden on host modules. See the note on ExportedFunction interface")
353}
354
355// Instantiate implements HostModuleBuilder.Instantiate
356func (b *hostModuleBuilder) Instantiate(ctx context.Context) (api.Module, error) {
357	if compiled, err := b.Compile(ctx); err != nil {
358		return nil, err
359	} else {
360		compiled.(*compiledModule).closeWithModule = true
361		m, err := b.r.InstantiateModule(ctx, compiled, NewModuleConfig())
362		if err != nil {
363			return nil, err
364		}
365		return hostModuleInstance{m}, nil
366	}
367}