compiler: move wasm ABI workaround to transform package
By considering this as a regular transformation, it can be easily tested.
Этот коммит содержится в:
родитель
91299b6466
коммит
4d79d473c4
6 изменённых файлов: 248 добавлений и 133 удалений
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
28
transform/testdata/wasm-abi.ll
предоставленный
Обычный файл
28
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
|
||||
}
|
45
transform/testdata/wasm-abi.out.ll
предоставленный
Обычный файл
45
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
|
||||
}
|
155
transform/wasm-abi.go
Обычный файл
155
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
|
||||
}
|
18
transform/wasm-abi_test.go
Обычный файл
18
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)
|
||||
}
|
||||
})
|
||||
}
|
Загрузка…
Создание таблицы
Сослаться в новой задаче