package main import ( gcontext "context" "errors" "io" "log" "os" "reflect" "strings" "github.com/Xe/ln" "github.com/go-interpreter/wagon/exec" "github.com/go-interpreter/wagon/validate" "github.com/go-interpreter/wagon/wasm" "github.com/spf13/afero" ) // Process is a larger level wrapper around a webassembly VM that gives it // system call access. type Process struct { id int32 vm *exec.VM mod *wasm.Module fs afero.Fs files []afero.File name string // go runtime cruft goRuntimeValues []interface{} } // NewProcess constructs a new webassembly process based on the input webassembly module as a reader. func NewProcess(fin io.Reader, name string) (*Process, error) { p := &Process{} mod, err := wasm.ReadModule(fin, p.importer) if err != nil { return nil, err } if mod.Memory == nil { return nil, errors.New("must declare a memory, sorry :(") } vm, err := exec.NewVM(mod) if err != nil { return nil, err } p.mod = mod p.vm = vm p.fs = afero.NewMemMapFs() return p, nil } func (p *Process) importer(name string) (*wasm.Module, error) { switch name { case "env": m := wasm.NewModule() m.Types = &wasm.SectionTypes{ Entries: []wasm.FunctionSig{ { Form: 0, ParamTypes: []wasm.ValueType{wasm.ValueTypeI32, wasm.ValueTypeI32}, ReturnTypes: []wasm.ValueType{wasm.ValueTypeI32}, }, { Form: 0, ParamTypes: []wasm.ValueType{wasm.ValueTypeI32, wasm.ValueTypeI32, wasm.ValueTypeI32}, ReturnTypes: []wasm.ValueType{wasm.ValueTypeI32}, }, { Form: 0, ParamTypes: []wasm.ValueType{wasm.ValueTypeI32}, ReturnTypes: []wasm.ValueType{wasm.ValueTypeI32}, }, }, } m.FunctionIndexSpace = []wasm.Function{ { Sig: &m.Types.Entries[0], Host: reflect.ValueOf(p.log), Body: &wasm.FunctionBody{}, }, { Sig: &m.Types.Entries[0], Host: reflect.ValueOf(p.open), Body: &wasm.FunctionBody{}, }, { Sig: &m.Types.Entries[1], Host: reflect.ValueOf(p.write), Body: &wasm.FunctionBody{}, }, { Sig: &m.Types.Entries[1], Host: reflect.ValueOf(p.read), Body: &wasm.FunctionBody{}, }, { Sig: &m.Types.Entries[2], Host: reflect.ValueOf(p.close), Body: &wasm.FunctionBody{}, }, { Sig: &m.Types.Entries[2], Host: reflect.ValueOf(isatty), Body: &wasm.FunctionBody{}, }, { Sig: &m.Types.Entries[2], Host: reflect.ValueOf(p.unlink), Body: &wasm.FunctionBody{}, }, } m.Export = &wasm.SectionExports{ Entries: map[string]wasm.ExportEntry{ "log": { FieldStr: "log", Kind: wasm.ExternalFunction, Index: 0, }, "open": { FieldStr: "open", Kind: wasm.ExternalFunction, Index: 1, }, "write": { FieldStr: "write", Kind: wasm.ExternalFunction, Index: 2, }, "read": { FieldStr: "read", Kind: wasm.ExternalFunction, Index: 3, }, "close": { FieldStr: "close", Kind: wasm.ExternalFunction, Index: 4, }, "isatty": { FieldStr: "isatty", Kind: wasm.ExternalFunction, Index: 5, }, "unlink": { FieldStr: "unlink", Kind: wasm.ExternalFunction, Index: 6, }, }, } return m, nil default: f, err := os.Open(name + ".wasm") if err != nil { return nil, err } defer f.Close() m, err := wasm.ReadModule(f, nil) if err != nil { return nil, err } err = validate.VerifyModule(m) if err != nil { return nil, err } return m, nil } } // ID returns the process ID. func (p Process) ID() int32 { return p.id } // VM returns the webassembly VM used to execute code. This isn't thread-safe. func (p Process) VM() *exec.VM { return p.vm } func (p *Process) log(ptr int32, len int32) int32 { mem := p.vm.Memory() if mem == nil { panic("memory is nil, wtf") } data := mem[ptr : ptr+len] ln.Log(gcontext.Background(), ln.F{"data": data, "data_string": string(data), "pid": p.id}) return 0 } func (p *Process) writeMem(ptr int32, data []byte) (int, error) { mem := p.vm.Memory() if mem == nil { return 0, errors.New("wtf") } for i, d := range data { mem[ptr+int32(i)] = d } return len(data), nil } func (p *Process) readMem(ptr int32) []byte { var result []byte mem := p.vm.Memory()[ptr:] for _, bt := range mem { if bt == 0 { return result } result = append(result, bt) } return result } func (p *Process) open(fnamesP int32, flags int32) int32 { str := string(p.readMem(fnamesP)) var fi afero.File var err error switch str { case "stdout": fi = stdFD{ ReadWriteCloser: os.Stdout, } case "log": fi = stdFD{ ReadWriteCloser: loggerFile{ Logger: log.New(os.Stdout, p.name+" ", log.LstdFlags), }, } default: fi, err = p.fs.OpenFile(string(str), int(flags), 0666) if err != nil { if strings.Contains(err.Error(), afero.ErrFileNotFound.Error()) { fi, err = p.fs.Create(str) } } } if err != nil { panic(err) } fd := len(p.files) p.files = append(p.files, fi) return int32(fd) } func (p *Process) write(fd int32, ptr int32, len int32) int32 { data := p.vm.Memory()[ptr : ptr+len] n, err := p.files[fd].Write(data) if err != nil { panic(err) } return int32(n) } func (p *Process) read(fd int32, ptr int32, len int32) int32 { data := make([]byte, len) na, err := p.files[fd].Read(data) if err != nil { panic(err) } nb, err := p.writeMem(ptr, data) if err != nil { panic(err) } if na != nb { panic("did not copy the same number of bytes???") } return int32(na) } func (p *Process) close(fd int32) int32 { f := p.files[fd] err := f.Close() if err != nil { panic(err) } if len(p.files) == 1 { p.files = []afero.File{} } else { p.files = append(p.files[:fd], p.files[fd+1]) } return 0 } func (p *Process) unlink(fnameP int32) int32 { fname := string(p.readMem(fnameP)) err := p.fs.RemoveAll(fname) if err != nil { panic(err) } return 0 } func isatty(fd int32) int32 { return 0 } func (p *Process) Main() (uint32, error) { foundMain := false mainID := uint32(0) for name, entry := range p.mod.Export.Entries { if name == "main" && entry.FieldStr == "main" { mainID = entry.Index foundMain = true break } } if !foundMain { return 1, errors.New("no main function found") } returnCode, err := p.vm.ExecCode(int64(mainID)) if err != nil { return 1, err } return returnCode.(uint32), nil }