Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions spx-gui/build-wasm.sh
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,7 @@ cp ../tools/spxls/spxls-pkgdata.zip src/assets/wasm/spxls-pkgdata.zip
# Build and copy ispx.wasm
( cd ../tools/ispx && ./build.sh )
cp ../tools/ispx/ispx.wasm src/assets/wasm/ispx.wasm

# Build and copy the generic XGo executor.
( cd ../tools/xgoexec && bash build.sh )
cp ../tools/xgoexec/xgoexec.wasm src/assets/wasm/xgoexec.wasm
27 changes: 27 additions & 0 deletions spx-gui/src/apps/xbuilder/pages/debug/xgo-executor.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<script setup lang="ts">
import { ref } from 'vue'
import { XGoExecutor } from '@/utils/xgo-executor'
import { UIButton, UICard, useMessage } from '@/components/ui'

const message = useMessage()
const result = ref('')

async function runPlain() {
result.value = ''
try { await new XGoExecutor('none', message.info).run({ 'main.xgo': 'echo "plain XGo is running"' }); result.value = 'plain XGo ran successfully' } catch (error) { result.value = String(error) }
}
async function runTutorial() {
result.value = ''
try { await new XGoExecutor('tutorial', message.info).run({ 'main.course': 'onStart => {\n showMessage "Tutorial course started"\n}' }); result.value = 'tutorial class framework ran successfully' } catch (error) { result.value = String(error) }
}
</script>

<template>
<main class="mx-auto max-w-2xl p-8">
<h1 class="mb-6 text-2xl font-semibold">XGo executor debug</h1>
<UICard class="p-6">
<div class="flex gap-3"><UIButton type="primary" @click="runPlain">Run plain XGo</UIButton><UIButton type="secondary" @click="runTutorial">Run tutorial course</UIButton></div>
<pre v-if="result !== ''" class="mt-5 whitespace-pre-wrap">{{ result }}</pre>
</UICard>
</main>
</template>
4 changes: 4 additions & 0 deletions spx-gui/src/apps/xbuilder/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,10 @@ const routes: Array<RouteRecordRaw> = [
path: '/tutorials',
component: () => import('./pages/tutorials/index.vue')
},
{
path: '/debug/xgo-executor',
component: () => import('./pages/debug/xgo-executor.vue')
},
{
path: '/course/:courseSeriesIdInput/:courseIdInput/start',
component: () => import('./pages/tutorials/course-start.vue'),
Expand Down
40 changes: 40 additions & 0 deletions spx-gui/src/utils/xgo-executor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import '@/assets/wasm/wasm_exec.js'

const wasmUrl = new URL('@/assets/wasm/xgoexec.wasm', import.meta.url).href

declare class Go { importObject: WebAssembly.Imports; run(instance: WebAssembly.Instance): Promise<void> }

type XGoExecutorGlobal = Window & {
xbuilder_xgoexec_configure: (framework: string) => void
xbuilder_xgoexec_build: (files: Record<string, Uint8Array>) => void
xbuilder_xgoexec_run: () => void
xbuilder_xgoexec_stop: () => void
xbuilder_xgoexec_error: (phase: string, message: string) => void
xbuilder_xgoexec_capability: (name: string, content: string) => void
}

export class XGoExecutor {
private readonly target = window as XGoExecutorGlobal

constructor(private framework: 'none' | 'tutorial', private onMessage: (content: string) => void) {}

async run(files: Record<string, string>) {
const go = new Go()
const { instance } = await WebAssembly.instantiateStreaming(fetch(wasmUrl), go.importObject)
const errors: string[] = []
this.target.xbuilder_xgoexec_error = (phase, message) => errors.push(`${phase}: ${message}`)
Comment thread
nighca marked this conversation as resolved.
Outdated
this.target.xbuilder_xgoexec_capability = (name, content) => {
if (name !== 'showMessage') throw new Error(`unsupported capability: ${name}`)
this.onMessage(content)
}
void go.run(instance)
await new Promise((resolve) => window.setTimeout(resolve, 0))
this.target.xbuilder_xgoexec_configure(this.framework)
Comment thread
nighca marked this conversation as resolved.
Outdated
const encoded = Object.fromEntries(Object.entries(files).map(([path, content]) => [path, new TextEncoder().encode(content)]))
this.target.xbuilder_xgoexec_build(encoded)
if (errors.length > 0) throw new Error(errors.join('\n'))
this.target.xbuilder_xgoexec_run()
}

stop() { this.target.xbuilder_xgoexec_stop() }
}
4 changes: 4 additions & 0 deletions tools/xgoexec/build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/bin/bash
set -e
go generate ./...
GOOS=js GOARCH=wasm go build -trimpath -ldflags "-s -w" -o xgoexec.wasm
5 changes: 5 additions & 0 deletions tools/xgoexec/embedded_pkgs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package main

//go:generate go tool qexp -outdir internal/pkg github.com/goplus/builder/tools/xgoexec/tutorial

import _ "github.com/goplus/builder/tools/xgoexec/internal/pkg/github.com/goplus/builder/tools/xgoexec/tutorial"
25 changes: 25 additions & 0 deletions tools/xgoexec/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
module github.com/goplus/builder/tools/xgoexec

go 1.25.0

tool github.com/goplus/ixgo/cmd/qexp

require (
github.com/goplus/builder/tools/xgoexec/tutorial v0.0.0
github.com/goplus/ixgo v1.1.0
github.com/goplus/mod v0.20.2
)

require (
github.com/goplus/gogen v1.23.0-pre.3.0.20260414234848-6641c10c9d6f // indirect
github.com/goplus/reflectx v1.7.0 // indirect
github.com/goplus/xgo v1.7.2-0.20260414235301-df19f4a1b7c2 // indirect
github.com/qiniu/x v1.17.0 // indirect
github.com/timandy/routine v1.1.5 // indirect
github.com/visualfc/funcval v0.1.4 // indirect
github.com/visualfc/xtype v0.3.0 // indirect
golang.org/x/mod v0.29.0 // indirect
golang.org/x/tools v0.38.0 // indirect
)

replace github.com/goplus/builder/tools/xgoexec/tutorial => ./tutorial
40 changes: 40 additions & 0 deletions tools/xgoexec/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/goplus/gogen v1.23.0-pre.3.0.20260414234848-6641c10c9d6f h1:qb3VcmljqvrIgEVbs83cbZkckszvtHl3OnO/ums8Zoc=
github.com/goplus/gogen v1.23.0-pre.3.0.20260414234848-6641c10c9d6f/go.mod h1:Y7ulYW3wonQ3d9er00b0uGFEV/IUZa6okWJZh892ACQ=
github.com/goplus/ixgo v1.1.0 h1:Lrp3xTzN+xUsrb7H3bIsGMHna1GTmoCrljDhFBF8OOs=
github.com/goplus/ixgo v1.1.0/go.mod h1:fhd/bwm497c66k7hZJJ2WmmvPiKEAJUQR3f2oQiAANQ=
github.com/goplus/mod v0.20.2 h1:YX72E6AhhRLvlkVnI9cBK6PZvUwtge2hwROh7w9N6Yk=
github.com/goplus/mod v0.20.2/go.mod h1:lWW62tH7L3Vm42Lr6wlUMYHvsm5w3TkEpE2ulKTgmU8=
github.com/goplus/reflectx v1.7.0 h1:52l0Si+vDc6CvT7Y+h47h2QUs5HQp6qxPlmvLsMzo38=
github.com/goplus/reflectx v1.7.0/go.mod h1:EXX8KSPTmnb2W1PfyVkDC0Qwet0lYX2h2/S06Vhwrhw=
github.com/goplus/xgo v1.7.2-0.20260414235301-df19f4a1b7c2 h1:kXIGYlJUTii+7nv1XTKR0ctMZSeTcgYMoQuEexZPr7A=
github.com/goplus/xgo v1.7.2-0.20260414235301-df19f4a1b7c2/go.mod h1:fX3+ZaYEzBE6qkgFcJx83D6DbUo/vMO8Y1Allydy2ws=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/qiniu/x v1.17.0 h1:OsyDKXzYp5vw9Hc7VAe4Cso1Sp50fLKGsBuDteyTevE=
github.com/qiniu/x v1.17.0/go.mod h1:AiovSOCaRijaf3fj+0CBOpR1457pn24b0Vdb1JpwhII=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/timandy/routine v1.1.5 h1:LSpm7Iijwb9imIPlucl4krpr2EeCeAUvifiQ9Uf5X+M=
github.com/timandy/routine v1.1.5/go.mod h1:kXslgIosdY8LW0byTyPnenDgn4/azt2euufAq9rK51w=
github.com/visualfc/funcval v0.1.4 h1:lAI88zQYfRzmC7mKF4+swXeCZvb8wb1f3lMSDRAY2mQ=
github.com/visualfc/funcval v0.1.4/go.mod h1:3Izv+irhArmrTvy+lmL6pIq16gSOzx73CIka51J9eR0=
github.com/visualfc/goembed v0.3.4 h1:foORJ0KT/pKwA23dStPFOtxaW7F96PnNSiLXHVXrfVk=
github.com/visualfc/goembed v0.3.4/go.mod h1:jCVCz/yTJGyslo6Hta+pYxWWBuq9ADCcIVZBTQ0/iVI=
github.com/visualfc/xtype v0.3.0 h1:K0Oo5XMcSjv+ohqn8L6RjgOZ9dNwfzPv3/AIV1SXRfA=
github.com/visualfc/xtype v0.3.0/go.mod h1:VYIH9S2bmdWKlBb7c725ES6yKF9+pyHBU2SFNqGVMGM=
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// export by github.com/goplus/ixgo/cmd/qexp

package tutorial

import (
q "github.com/goplus/builder/tools/xgoexec/tutorial"

"reflect"

"github.com/goplus/ixgo"
)

func init() {
ixgo.RegisterPackage(&ixgo.Package{
Name: "tutorial",
Path: "github.com/goplus/builder/tools/xgoexec/tutorial",
Deps: map[string]string{},
Interfaces: map[string]reflect.Type{},
NamedTypes: map[string]reflect.Type{
"Course": reflect.TypeOf((*q.Course)(nil)).Elem(),
},
AliasTypes: map[string]reflect.Type{},
Vars: map[string]reflect.Value{},
Funcs: map[string]reflect.Value{
"SetShowMessage": reflect.ValueOf(q.SetShowMessage),
},
TypedConsts: map[string]ixgo.TypedConst{},
UntypedConsts: map[string]ixgo.UntypedConst{},
})
}
174 changes: 174 additions & 0 deletions tools/xgoexec/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
//go:build js && wasm

package main

import (
"context"
"fmt"
"io/fs"
"path"
"strings"
"sync"
"syscall/js"

"github.com/goplus/builder/tools/xgoexec/tutorial"
"github.com/goplus/ixgo"
"github.com/goplus/ixgo/xgobuild"
"github.com/goplus/mod/modfile"
)

var state struct {
sync.Mutex
ctx *ixgo.Context
interp *ixgo.Interp
cancel context.CancelFunc
framework string
}

func init() {
js.Global().Set("xbuilder_xgoexec_configure", js.FuncOf(configure))
js.Global().Set("xbuilder_xgoexec_build", js.FuncOf(build))
js.Global().Set("xbuilder_xgoexec_run", js.FuncOf(run))
js.Global().Set("xbuilder_xgoexec_stop", js.FuncOf(stop))
}

func configure(this js.Value, args []js.Value) any {
state.Lock()
defer state.Unlock()
if state.ctx != nil {
return jsError("already configured")
}
ctx := ixgo.NewContext(ixgo.SupportMultipleInterp | xgobuild.StaticLoad)
ctx.SetPanic(func(info *ixgo.PanicInfo) { js.Global().Call("xbuilder_xgoexec_error", "runtime", info.Error.Error()) })
ctx.Loader.Import("fmt")
if len(args) > 0 && args[0].String() == "tutorial" {
xgobuild.RegisterProject(&modfile.Project{Ext: ".course", Class: "Course", PkgPaths: []string{"github.com/goplus/builder/tools/xgoexec/tutorial"}})
tutorial.SetShowMessage(func(content string) { js.Global().Call("xbuilder_xgoexec_capability", "showMessage", content) })
}
if len(args) > 0 {
state.framework = args[0].String()
}
state.ctx = ctx
return nil
}

func build(this js.Value, args []js.Value) any {
if len(args) == 0 {
return jsError("missing files")
}
files := map[string][]byte{}
keys := js.Global().Get("Object").Call("keys", args[0])
for i := 0; i < keys.Length(); i++ {
name := keys.Index(i).String()
value := args[0].Get(name)
data := make([]byte, value.Length())
js.CopyBytesToGo(data, value)
files[name] = data
}
state.Lock()
defer state.Unlock()
if state.ctx == nil {
return jsError("not configured")
}
Comment thread
nighca marked this conversation as resolved.
Outdated
source, err := xgobuild.BuildFSDir(state.ctx, mapFS(files), ".")
if err != nil {
return jsError(err.Error())
}
if state.framework == "tutorial" {
source = []byte(strings.Replace(string(source), "new(Course).Main()", "new(Course).MainEntry()", 1))
}
Comment thread
nighca marked this conversation as resolved.
Outdated
pkg, err := state.ctx.LoadFile("main.go", source)
if err != nil {
return jsError(err.Error())
}
interp, err := state.ctx.NewInterp(pkg)
if err != nil {
return jsError(err.Error())
}
state.interp = interp
return nil
}

func run(this js.Value, args []js.Value) any {
state.Lock()
if state.ctx == nil || state.interp == nil {
state.Unlock()
return jsError("not built")
}
ctx, interp := state.ctx, state.interp
runCtx, cancel := context.WithCancel(context.Background())
state.cancel = cancel
state.Unlock()
go func() {
ctx.RunContext = runCtx
_, err := ctx.RunInterp(interp, "main.go", nil)
if err != nil && runCtx.Err() == nil {
js.Global().Call("xbuilder_xgoexec_error", "runtime", err.Error())
}
}()
return nil
}
Comment thread
nighca marked this conversation as resolved.

func stop(this js.Value, args []js.Value) any {
state.Lock()
defer state.Unlock()
if state.cancel != nil {
state.cancel()
state.cancel = nil
}
return nil
}
func jsError(message string) any { return js.Global().Get("Error").New(message) }

type mapFS map[string][]byte

func (p mapFS) ReadFile(name string) ([]byte, error) {
value, ok := p[name]
if !ok {
return nil, fs.ErrNotExist
}
return value, nil
}
func (p mapFS) ReadDir(dirname string) ([]fs.DirEntry, error) {
prefix := ""
if dirname != "." {
prefix = dirname + "/"
}
entries := map[string]bool{}
for filename := range p {
if !strings.HasPrefix(filename, prefix) {
continue
}
name := strings.TrimPrefix(filename, prefix)
if i := strings.IndexByte(name, '/'); i >= 0 {
entries[name[:i]] = true
} else {
entries[name] = false
}
}
result := make([]fs.DirEntry, 0, len(entries))
for name, isDir := range entries {
result = append(result, mapDirEntry{name: name, dir: isDir})
}
return result, nil
}
func (p mapFS) Join(elem ...string) string { return path.Join(elem...) }
func (p mapFS) Base(filename string) string { return path.Base(filename) }
func (p mapFS) Abs(filename string) (string, error) { return path.Join("/", filename), nil }

type mapDirEntry struct {
name string
dir bool
}

func (p mapDirEntry) Name() string { return p.name }
func (p mapDirEntry) IsDir() bool { return p.dir }
func (p mapDirEntry) Type() fs.FileMode {
if p.dir {
return fs.ModeDir
}
return 0
}
func (p mapDirEntry) Info() (fs.FileInfo, error) { return nil, fmt.Errorf("not implemented") }

func main() { select {} }
3 changes: 3 additions & 0 deletions tools/xgoexec/tutorial/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module github.com/goplus/builder/tools/xgoexec/tutorial

go 1.25.0
Loading
Loading