diff --git a/builder/build.go b/builder/build.go index fa4c689f..1bc06af4 100644 --- a/builder/build.go +++ b/builder/build.go @@ -16,6 +16,7 @@ import ( "github.com/tinygo-org/tinygo/compiler" "github.com/tinygo-org/tinygo/goenv" "github.com/tinygo-org/tinygo/interp" + "github.com/tinygo-org/tinygo/transform" ) // Build performs a single package to executable Go build. It takes in a package @@ -61,7 +62,7 @@ func Build(pkgName, outpath string, config *compileopts.Config, action func(stri // stack-allocated values. // Use -wasm-abi=generic to disable this behaviour. if config.Options.WasmAbi == "js" && strings.HasPrefix(config.Triple(), "wasm") { - err := c.ExternalInt64AsPtr() + err := transform.ExternalInt64AsPtr(c.Module()) if err != nil { return err } diff --git a/compiler/compiler.go b/compiler/compiler.go index a161d2cf..4d3d50d2 100644 --- a/compiler/compiler.go +++ b/compiler/compiler.go @@ -2550,138 +2550,6 @@ func (c *Compiler) NonConstGlobals() { } } -// When -wasm-abi flag set to "js" (default), -// replace i64 in an external function with a stack-allocated i64*, to work -// around the lack of 64-bit integers in JavaScript (commonly used together with -// WebAssembly). Once that's resolved, this pass may be avoided. -// See also the -wasm-abi= flag -// https://github.com/WebAssembly/design/issues/1172 -func (c *Compiler) ExternalInt64AsPtr() error { - int64Type := c.ctx.Int64Type() - int64PtrType := llvm.PointerType(int64Type, 0) - for fn := c.mod.FirstFunction(); !fn.IsNil(); fn = llvm.NextFunction(fn) { - if fn.Linkage() != llvm.ExternalLinkage { - // Only change externally visible functions (exports and imports). - continue - } - if strings.HasPrefix(fn.Name(), "llvm.") || strings.HasPrefix(fn.Name(), "runtime.") { - // Do not try to modify the signature of internal LLVM functions and - // assume that runtime functions are only temporarily exported for - // coroutine lowering. - continue - } - - hasInt64 := false - paramTypes := []llvm.Type{} - - // Check return type for 64-bit integer. - fnType := fn.Type().ElementType() - returnType := fnType.ReturnType() - if returnType == int64Type { - hasInt64 = true - paramTypes = append(paramTypes, int64PtrType) - returnType = c.ctx.VoidType() - } - - // Check param types for 64-bit integers. - for param := fn.FirstParam(); !param.IsNil(); param = llvm.NextParam(param) { - if param.Type() == int64Type { - hasInt64 = true - paramTypes = append(paramTypes, int64PtrType) - } else { - paramTypes = append(paramTypes, param.Type()) - } - } - - if !hasInt64 { - // No i64 in the paramter list. - continue - } - - // Add $i64wrapper to the real function name as it is only used - // internally. - // Add a new function with the correct signature that is exported. - name := fn.Name() - fn.SetName(name + "$i64wrap") - externalFnType := llvm.FunctionType(returnType, paramTypes, fnType.IsFunctionVarArg()) - externalFn := llvm.AddFunction(c.mod, name, externalFnType) - - if fn.IsDeclaration() { - // Just a declaration: the definition doesn't exist on the Go side - // so it cannot be called from external code. - // Update all users to call the external function. - // The old $i64wrapper function could be removed, but it may as well - // be left in place. - for use := fn.FirstUse(); !use.IsNil(); use = use.NextUse() { - call := use.User() - c.builder.SetInsertPointBefore(call) - callParams := []llvm.Value{} - var retvalAlloca llvm.Value - if fnType.ReturnType() == int64Type { - retvalAlloca = c.builder.CreateAlloca(int64Type, "i64asptr") - callParams = append(callParams, retvalAlloca) - } - for i := 0; i < call.OperandsCount()-1; i++ { - operand := call.Operand(i) - if operand.Type() == int64Type { - // Pass a stack-allocated pointer instead of the value - // itself. - alloca := c.builder.CreateAlloca(int64Type, "i64asptr") - c.builder.CreateStore(operand, alloca) - callParams = append(callParams, alloca) - } else { - // Unchanged parameter. - callParams = append(callParams, operand) - } - } - if fnType.ReturnType() == int64Type { - // Pass a stack-allocated pointer as the first parameter - // where the return value should be stored, instead of using - // the regular return value. - c.builder.CreateCall(externalFn, callParams, call.Name()) - returnValue := c.builder.CreateLoad(retvalAlloca, "retval") - call.ReplaceAllUsesWith(returnValue) - call.EraseFromParentAsInstruction() - } else { - newCall := c.builder.CreateCall(externalFn, callParams, call.Name()) - call.ReplaceAllUsesWith(newCall) - call.EraseFromParentAsInstruction() - } - } - } else { - // The function has a definition in Go. This means that it may still - // be called both Go and from external code. - // Keep existing calls with the existing convention in place (for - // better performance), but export a new wrapper function with the - // correct calling convention. - fn.SetLinkage(llvm.InternalLinkage) - fn.SetUnnamedAddr(true) - entryBlock := c.ctx.AddBasicBlock(externalFn, "entry") - c.builder.SetInsertPointAtEnd(entryBlock) - var callParams []llvm.Value - if fnType.ReturnType() == int64Type { - return errors.New("not yet implemented: exported function returns i64 with -wasm-abi=js; " + - "see https://tinygo.org/compiler-internals/calling-convention/") - } - for i, origParam := range fn.Params() { - paramValue := externalFn.Param(i) - if origParam.Type() == int64Type { - paramValue = c.builder.CreateLoad(paramValue, "i64") - } - callParams = append(callParams, paramValue) - } - retval := c.builder.CreateCall(fn, callParams, "") - if retval.Type().TypeKind() == llvm.VoidTypeKind { - c.builder.CreateRetVoid() - } else { - c.builder.CreateRet(retval) - } - } - } - - return nil -} - // Emit object file (.o). func (c *Compiler) EmitObject(path string) error { llvmBuf, err := c.machine.EmitToMemoryBuffer(c.mod, llvm.ObjectFile) diff --git a/transform/testdata/wasm-abi.ll b/transform/testdata/wasm-abi.ll new file mode 100644 index 00000000..79e25347 --- /dev/null +++ b/transform/testdata/wasm-abi.ll @@ -0,0 +1,28 @@ +target datalayout = "e-m:e-p:32:32-i64:64-n32:64-S128" +target triple = "wasm32-unknown-unknown-wasm" + +declare i64 @externalCall(i8*, i32, i64) + +define internal i64 @testCall(i8* %ptr, i32 %len, i64 %foo) { + %val = call i64 @externalCall(i8* %ptr, i32 %len, i64 %foo) + ret i64 %val +} + +define internal i64 @testCallNonEntry(i8* %ptr, i32 %len) { +entry: + br label %bb1 + +bb1: + %val = call i64 @externalCall(i8* %ptr, i32 %len, i64 3) + ret i64 %val +} + +define void @exportedFunction(i64 %foo) { + %unused = shl i64 %foo, 1 + ret void +} + +define internal void @callExportedFunction(i64 %foo) { + call void @exportedFunction(i64 %foo) + ret void +} diff --git a/transform/testdata/wasm-abi.out.ll b/transform/testdata/wasm-abi.out.ll new file mode 100644 index 00000000..bc69ffbb --- /dev/null +++ b/transform/testdata/wasm-abi.out.ll @@ -0,0 +1,45 @@ +target datalayout = "e-m:e-p:32:32-i64:64-n32:64-S128" +target triple = "wasm32-unknown-unknown-wasm" + +declare i64 @"externalCall$i64wrap"(i8*, i32, i64) + +define internal i64 @testCall(i8* %ptr, i32 %len, i64 %foo) { + %i64asptr = alloca i64 + %i64asptr1 = alloca i64 + store i64 %foo, i64* %i64asptr1 + call void @externalCall(i64* %i64asptr, i8* %ptr, i32 %len, i64* %i64asptr1) + %retval = load i64, i64* %i64asptr + ret i64 %retval +} + +define internal i64 @testCallNonEntry(i8* %ptr, i32 %len) { +entry: + br label %bb1 + +bb1: ; preds = %entry + %i64asptr = alloca i64 + %i64asptr1 = alloca i64 + store i64 3, i64* %i64asptr1 + call void @externalCall(i64* %i64asptr, i8* %ptr, i32 %len, i64* %i64asptr1) + %retval = load i64, i64* %i64asptr + ret i64 %retval +} + +define internal void @"exportedFunction$i64wrap"(i64 %foo) unnamed_addr { + %unused = shl i64 %foo, 1 + ret void +} + +define internal void @callExportedFunction(i64 %foo) { + call void @"exportedFunction$i64wrap"(i64 %foo) + ret void +} + +declare void @externalCall(i64*, i8*, i32, i64*) + +define void @exportedFunction(i64*) { +entry: + %i64 = load i64, i64* %0 + call void @"exportedFunction$i64wrap"(i64 %i64) + ret void +} diff --git a/transform/wasm-abi.go b/transform/wasm-abi.go new file mode 100644 index 00000000..1d167b51 --- /dev/null +++ b/transform/wasm-abi.go @@ -0,0 +1,155 @@ +package transform + +import ( + "errors" + "strings" + + "tinygo.org/x/go-llvm" +) + +// ExternalInt64AsPtr converts i64 parameters in externally-visible functions to +// values passed by reference (*i64), to work around the lack of 64-bit integers +// in JavaScript (commonly used together with WebAssembly). Once that's +// resolved, this pass may be avoided. For more details: +// https://github.com/WebAssembly/design/issues/1172 +// +// This pass can be enabled/disabled with the -wasm-abi flag, and is enabled by +// default as of december 2019. +func ExternalInt64AsPtr(mod llvm.Module) error { + ctx := mod.Context() + builder := ctx.NewBuilder() + defer builder.Dispose() + int64Type := ctx.Int64Type() + int64PtrType := llvm.PointerType(int64Type, 0) + + for fn := mod.FirstFunction(); !fn.IsNil(); fn = llvm.NextFunction(fn) { + if fn.Linkage() != llvm.ExternalLinkage { + // Only change externally visible functions (exports and imports). + continue + } + if strings.HasPrefix(fn.Name(), "llvm.") || strings.HasPrefix(fn.Name(), "runtime.") { + // Do not try to modify the signature of internal LLVM functions and + // assume that runtime functions are only temporarily exported for + // coroutine lowering. + continue + } + + hasInt64 := false + paramTypes := []llvm.Type{} + + // Check return type for 64-bit integer. + fnType := fn.Type().ElementType() + returnType := fnType.ReturnType() + if returnType == int64Type { + hasInt64 = true + paramTypes = append(paramTypes, int64PtrType) + returnType = ctx.VoidType() + } + + // Check param types for 64-bit integers. + for param := fn.FirstParam(); !param.IsNil(); param = llvm.NextParam(param) { + if param.Type() == int64Type { + hasInt64 = true + paramTypes = append(paramTypes, int64PtrType) + } else { + paramTypes = append(paramTypes, param.Type()) + } + } + + if !hasInt64 { + // No i64 in the paramter list. + continue + } + + // Add $i64wrapper to the real function name as it is only used + // internally. + // Add a new function with the correct signature that is exported. + name := fn.Name() + fn.SetName(name + "$i64wrap") + externalFnType := llvm.FunctionType(returnType, paramTypes, fnType.IsFunctionVarArg()) + externalFn := llvm.AddFunction(mod, name, externalFnType) + + if fn.IsDeclaration() { + // Just a declaration: the definition doesn't exist on the Go side + // so it cannot be called from external code. + // Update all users to call the external function. + // The old $i64wrapper function could be removed, but it may as well + // be left in place. + for use := fn.FirstUse(); !use.IsNil(); use = use.NextUse() { + call := use.User() + builder.SetInsertPointBefore(call) + callParams := []llvm.Value{} + var retvalAlloca llvm.Value + if fnType.ReturnType() == int64Type { + retvalAlloca = builder.CreateAlloca(int64Type, "i64asptr") + callParams = append(callParams, retvalAlloca) + } + for i := 0; i < call.OperandsCount()-1; i++ { + operand := call.Operand(i) + if operand.Type() == int64Type { + // Pass a stack-allocated pointer instead of the value + // itself. + alloca := builder.CreateAlloca(int64Type, "i64asptr") + builder.CreateStore(operand, alloca) + callParams = append(callParams, alloca) + } else { + // Unchanged parameter. + callParams = append(callParams, operand) + } + } + var callName string + if returnType.TypeKind() != llvm.VoidTypeKind { + // Only use the name of the old call instruction if the new + // call is not a void call. + // A call instruction with an i64 return type may have had a + // name, but it cannot have a name after this transform + // because the return type will now be void. + callName = call.Name() + } + if fnType.ReturnType() == int64Type { + // Pass a stack-allocated pointer as the first parameter + // where the return value should be stored, instead of using + // the regular return value. + builder.CreateCall(externalFn, callParams, callName) + returnValue := builder.CreateLoad(retvalAlloca, "retval") + call.ReplaceAllUsesWith(returnValue) + call.EraseFromParentAsInstruction() + } else { + newCall := builder.CreateCall(externalFn, callParams, callName) + call.ReplaceAllUsesWith(newCall) + call.EraseFromParentAsInstruction() + } + } + } else { + // The function has a definition in Go. This means that it may still + // be called both Go and from external code. + // Keep existing calls with the existing convention in place (for + // better performance), but export a new wrapper function with the + // correct calling convention. + fn.SetLinkage(llvm.InternalLinkage) + fn.SetUnnamedAddr(true) + entryBlock := ctx.AddBasicBlock(externalFn, "entry") + builder.SetInsertPointAtEnd(entryBlock) + var callParams []llvm.Value + if fnType.ReturnType() == int64Type { + return errors.New("not yet implemented: exported function returns i64 with -wasm-abi=js; " + + "see https://tinygo.org/compiler-internals/calling-convention/") + } + for i, origParam := range fn.Params() { + paramValue := externalFn.Param(i) + if origParam.Type() == int64Type { + paramValue = builder.CreateLoad(paramValue, "i64") + } + callParams = append(callParams, paramValue) + } + retval := builder.CreateCall(fn, callParams, "") + if retval.Type().TypeKind() == llvm.VoidTypeKind { + builder.CreateRetVoid() + } else { + builder.CreateRet(retval) + } + } + } + + return nil +} diff --git a/transform/wasm-abi_test.go b/transform/wasm-abi_test.go new file mode 100644 index 00000000..abe5b30d --- /dev/null +++ b/transform/wasm-abi_test.go @@ -0,0 +1,18 @@ +package transform + +import ( + "testing" + + "tinygo.org/x/go-llvm" +) + +func TestWasmABI(t *testing.T) { + t.Parallel() + testTransform(t, "testdata/wasm-abi", func(mod llvm.Module) { + // Run ABI change pass. + err := ExternalInt64AsPtr(mod) + if err != nil { + t.Errorf("failed to change wasm ABI: %v", err) + } + }) +}