Skip to content

Commit 98b856c

Browse files
committed
fix: re-instantiate js instance on Reset (#159)
Rebuilds the instance instead of restoring a detached buffer; GC frees the old one.
1 parent 065023a commit 98b856c

1 file changed

Lines changed: 85 additions & 47 deletions

File tree

host-go/runtimes/js/runtime.go

Lines changed: 85 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -67,24 +67,32 @@ type wModule struct {
6767

6868
var _ module.Module = (*wModule)(nil)
6969

70-
func (m *wModule) NewInstance(functionName string, paramSets ...map[string]any) (module.Instance, error) {
71-
var nextFunction = func() module.MemSize { return 0 }
72-
// Register the `lens.next` function required as an import for wasm lens modules
73-
importObject := map[string]any{
74-
"lens": map[string]any{
75-
"next": js.FuncOf(func(this js.Value, args []js.Value) any {
76-
return nextFunction()
77-
}),
78-
},
79-
}
70+
// instanceHandles holds all per-instance js resources. Storing them behind a pointer lets
71+
// Reset swap the contents in-place so that the Alloc/Transform/Memory closures — which all
72+
// capture the same *instanceHandles — automatically see the fresh instance.
73+
type instanceHandles struct {
74+
instance js.Value
75+
memory js.Value
76+
alloc js.Value
77+
transform js.Value
78+
}
8079

80+
// newInstanceHandles instantiates the module with the given importObject and applies
81+
// set_param when params is non-empty.
82+
func newInstanceHandles(
83+
rt *wRuntime,
84+
mod js.Value,
85+
fnName string,
86+
params map[string]any,
87+
importObject map[string]any,
88+
) (*instanceHandles, error) {
8189
// Instantiates a WebAssembly.Instance from a WebAssembly.Module with imports.
8290
//
8391
// https://developer.mozilla.org/en-US/docs/WebAssembly/JavaScript_interface/instantiate_static
84-
promise := m.runtime.webAssembly.Call("instantiate", m.module, importObject)
92+
promise := rt.webAssembly.Call("instantiate", mod, importObject)
8593
results, err := await(promise)
8694
if err != nil {
87-
return module.Instance{}, err
95+
return nil, err
8896
}
8997
instance := results[0]
9098

@@ -98,37 +106,28 @@ func (m *wModule) NewInstance(functionName string, paramSets ...map[string]any)
98106
// https://developer.mozilla.org/en-US/docs/WebAssembly/JavaScript_interface/Memory
99107
memory := exports.Get("memory")
100108
if memory.Type() != js.TypeObject {
101-
return module.Instance{}, errors.New(fmt.Sprintf("Export `%s` does not exist", "memory"))
109+
return nil, fmt.Errorf("Export `%s` does not exist", "memory")
102110
}
103111

104112
alloc := exports.Get("alloc")
105113
if alloc.Type() != js.TypeFunction {
106-
return module.Instance{}, errors.New(fmt.Sprintf("Export `%s` does not exist", "alloc"))
114+
return nil, fmt.Errorf("Export `%s` does not exist", "alloc")
107115
}
108116

109-
transform := exports.Get(functionName)
117+
transform := exports.Get(fnName)
110118
if transform.Type() != js.TypeFunction {
111-
return module.Instance{}, errors.New(fmt.Sprintf("Export `%s` does not exist", functionName))
112-
}
113-
114-
params := map[string]any{}
115-
// Merge the param sets into a single map in case more than
116-
// one map is provided.
117-
for _, paramSet := range paramSets {
118-
for key, value := range paramSet {
119-
params[key] = value
120-
}
119+
return nil, fmt.Errorf("Export `%s` does not exist", fnName)
121120
}
122121

123122
if len(params) > 0 {
124123
setParam := exports.Get("set_param")
125124
if setParam.Type() != js.TypeFunction {
126-
return module.Instance{}, errors.New(fmt.Sprintf("Export `%s` does not exist", "set_param"))
125+
return nil, fmt.Errorf("Export `%s` does not exist", "set_param")
127126
}
128127

129128
sourceBytes, err := json.Marshal(params)
130129
if err != nil {
131-
return module.Instance{}, err
130+
return nil, err
132131
}
133132

134133
// allocate memory to write to
@@ -138,7 +137,7 @@ func (m *wModule) NewInstance(functionName string, paramSets ...map[string]any)
138137

139138
err = pipes.WriteItem(w, module.JSONTypeID, sourceBytes)
140139
if err != nil {
141-
return module.Instance{}, err
140+
return nil, err
142141
}
143142

144143
// set param from JavaScript memory
@@ -149,46 +148,85 @@ func (m *wModule) NewInstance(functionName string, paramSets ...map[string]any)
149148
// from memory using `pipes.GetItem`.
150149
id, data, err := pipes.ReadItem(r)
151150
if id.IsError() {
152-
return module.Instance{}, errors.New(string(data))
151+
return nil, errors.New(string(data))
153152
}
154153
if err != nil {
155-
return module.Instance{}, err
154+
return nil, err
156155
}
157156
}
158157

159-
jsMemory := js.Global().Get("Uint8Array").New(memory.Get("buffer"))
160-
initialLen := jsMemory.Get("length").Int()
161-
initialState := make([]byte, initialLen)
162-
js.CopyBytesToGo(initialState, jsMemory)
158+
return &instanceHandles{
159+
instance: instance,
160+
memory: memory,
161+
alloc: alloc,
162+
transform: transform,
163+
}, nil
164+
}
165+
166+
func (m *wModule) NewInstance(functionName string, paramSets ...map[string]any) (module.Instance, error) {
167+
params := map[string]any{}
168+
// Merge the param sets into a single map in case more than
169+
// one map is provided.
170+
for _, paramSet := range paramSets {
171+
for key, value := range paramSet {
172+
params[key] = value
173+
}
174+
}
175+
176+
var nextFn = func() module.MemSize { return 0 }
177+
// Register the `lens.next` function required as an import for wasm lens modules. The
178+
// import object (and its single js.Func) is created once and reused across re-instantiation
179+
// so Reset does not churn js callback registrations.
180+
importObject := map[string]any{
181+
"lens": map[string]any{
182+
"next": js.FuncOf(func(this js.Value, args []js.Value) any {
183+
return nextFn()
184+
}),
185+
},
186+
}
187+
188+
handles, err := newInstanceHandles(m.runtime, m.module, functionName, params, importObject)
189+
if err != nil {
190+
return module.Instance{}, err
191+
}
192+
193+
var resetErr error
163194

164195
return module.Instance{
165196
Alloc: func(u module.MemSize) (module.MemSize, error) {
166-
result := alloc.Invoke(int32(u))
197+
if resetErr != nil {
198+
return 0, resetErr
199+
}
200+
result := handles.alloc.Invoke(int32(u))
167201
return module.MemSize(result.Int()), nil
168202
},
169203
Transform: func(next func() module.MemSize) (module.MemSize, error) {
204+
if resetErr != nil {
205+
return 0, resetErr
206+
}
170207
// By assigning the next function immediately prior to calling transform, we allow multiple
171208
// pipeline stages to share the same wasm instance - provided they are not called concurrently.
172209
// This also allows module state to be shared across pipeline stages.
173-
nextFunction = next
174-
result := transform.Invoke()
210+
nextFn = next
211+
result := handles.transform.Invoke()
175212
return module.MemSize(result.Int()), nil
176213
},
177214
Memory: func() module.Memory {
178-
buffer := memory.Get("buffer")
179-
return newMemory(buffer)
215+
return newMemory(handles.memory.Get("buffer"))
180216
},
181217
Reset: func() {
182-
initialLen := len(initialState)
183-
currentLen := jsMemory.Get("length").Int()
184-
185-
js.CopyBytesToJS(jsMemory, initialState)
186-
187-
for i := initialLen; i < currentLen; i++ {
188-
jsMemory.SetIndex(i, js.ValueOf(0))
218+
nextFn = func() module.MemSize { return 0 }
219+
newHandles, err := newInstanceHandles(m.runtime, m.module, functionName, params, importObject)
220+
if err != nil {
221+
resetErr = err
222+
return
189223
}
224+
resetErr = nil
225+
*handles = *newHandles
226+
// The displaced WebAssembly.Instance has no explicit close; dropping the reference
227+
// lets the runtime GC reclaim its linear memory (issue #159).
190228
},
191-
OwnedBy: instance,
229+
OwnedBy: handles,
192230
}, nil
193231
}
194232

0 commit comments

Comments
 (0)