Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
3 changes: 2 additions & 1 deletion builder/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Expand Down
132 changes: 0 additions & 132 deletions compiler/compiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -2551,138 +2551,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)
Expand Down
28 changes: 28 additions & 0 deletions transform/testdata/wasm-abi.ll
Original file line number Diff line number Diff line change
@@ -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 changes: 45 additions & 0 deletions transform/testdata/wasm-abi.out.ll
Original file line number Diff line number Diff line change
@@ -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:
%i64asptr = alloca i64
%i64asptr1 = alloca i64
br label %bb1

bb1: ; preds = %entry
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
}
161 changes: 161 additions & 0 deletions transform/wasm-abi.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
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)

// This builder is only used for creating new allocas in the entry block of
// a function, avoiding many SetInsertPoint* calls.
entryBlockBuilder := ctx.NewBuilder()
defer entryBlockBuilder.Dispose()

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()
entryBlockBuilder.SetInsertPointBefore(call.InstructionParent().Parent().EntryBasicBlock().FirstInstruction())
builder.SetInsertPointBefore(call)
callParams := []llvm.Value{}
var retvalAlloca llvm.Value
if fnType.ReturnType() == int64Type {
retvalAlloca = entryBlockBuilder.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 := entryBlockBuilder.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
}
Loading