|
| 1 | +package transform |
| 2 | + |
| 3 | +import ( |
| 4 | + "errors" |
| 5 | + "strings" |
| 6 | + |
| 7 | + "tinygo.org/x/go-llvm" |
| 8 | +) |
| 9 | + |
| 10 | +// ExternalInt64AsPtr converts i64 parameters in externally-visible functions to |
| 11 | +// values passed by reference (*i64), to work around the lack of 64-bit integers |
| 12 | +// in JavaScript (commonly used together with WebAssembly). Once that's |
| 13 | +// resolved, this pass may be avoided. For more details: |
| 14 | +// https://github.com/WebAssembly/design/issues/1172 |
| 15 | +// |
| 16 | +// This pass can be enabled/disabled with the -wasm-abi flag, and is enabled by |
| 17 | +// default as of december 2019. |
| 18 | +func ExternalInt64AsPtr(mod llvm.Module) error { |
| 19 | + ctx := mod.Context() |
| 20 | + builder := ctx.NewBuilder() |
| 21 | + defer builder.Dispose() |
| 22 | + int64Type := ctx.Int64Type() |
| 23 | + int64PtrType := llvm.PointerType(int64Type, 0) |
| 24 | + |
| 25 | + for fn := mod.FirstFunction(); !fn.IsNil(); fn = llvm.NextFunction(fn) { |
| 26 | + if fn.Linkage() != llvm.ExternalLinkage { |
| 27 | + // Only change externally visible functions (exports and imports). |
| 28 | + continue |
| 29 | + } |
| 30 | + if strings.HasPrefix(fn.Name(), "llvm.") || strings.HasPrefix(fn.Name(), "runtime.") { |
| 31 | + // Do not try to modify the signature of internal LLVM functions and |
| 32 | + // assume that runtime functions are only temporarily exported for |
| 33 | + // coroutine lowering. |
| 34 | + continue |
| 35 | + } |
| 36 | + |
| 37 | + hasInt64 := false |
| 38 | + paramTypes := []llvm.Type{} |
| 39 | + |
| 40 | + // Check return type for 64-bit integer. |
| 41 | + fnType := fn.Type().ElementType() |
| 42 | + returnType := fnType.ReturnType() |
| 43 | + if returnType == int64Type { |
| 44 | + hasInt64 = true |
| 45 | + paramTypes = append(paramTypes, int64PtrType) |
| 46 | + returnType = ctx.VoidType() |
| 47 | + } |
| 48 | + |
| 49 | + // Check param types for 64-bit integers. |
| 50 | + for param := fn.FirstParam(); !param.IsNil(); param = llvm.NextParam(param) { |
| 51 | + if param.Type() == int64Type { |
| 52 | + hasInt64 = true |
| 53 | + paramTypes = append(paramTypes, int64PtrType) |
| 54 | + } else { |
| 55 | + paramTypes = append(paramTypes, param.Type()) |
| 56 | + } |
| 57 | + } |
| 58 | + |
| 59 | + if !hasInt64 { |
| 60 | + // No i64 in the paramter list. |
| 61 | + continue |
| 62 | + } |
| 63 | + |
| 64 | + // Add $i64wrapper to the real function name as it is only used |
| 65 | + // internally. |
| 66 | + // Add a new function with the correct signature that is exported. |
| 67 | + name := fn.Name() |
| 68 | + fn.SetName(name + "$i64wrap") |
| 69 | + externalFnType := llvm.FunctionType(returnType, paramTypes, fnType.IsFunctionVarArg()) |
| 70 | + externalFn := llvm.AddFunction(mod, name, externalFnType) |
| 71 | + |
| 72 | + if fn.IsDeclaration() { |
| 73 | + // Just a declaration: the definition doesn't exist on the Go side |
| 74 | + // so it cannot be called from external code. |
| 75 | + // Update all users to call the external function. |
| 76 | + // The old $i64wrapper function could be removed, but it may as well |
| 77 | + // be left in place. |
| 78 | + for use := fn.FirstUse(); !use.IsNil(); use = use.NextUse() { |
| 79 | + call := use.User() |
| 80 | + builder.SetInsertPointBefore(call) |
| 81 | + callParams := []llvm.Value{} |
| 82 | + var retvalAlloca llvm.Value |
| 83 | + if fnType.ReturnType() == int64Type { |
| 84 | + retvalAlloca = builder.CreateAlloca(int64Type, "i64asptr") |
| 85 | + callParams = append(callParams, retvalAlloca) |
| 86 | + } |
| 87 | + for i := 0; i < call.OperandsCount()-1; i++ { |
| 88 | + operand := call.Operand(i) |
| 89 | + if operand.Type() == int64Type { |
| 90 | + // Pass a stack-allocated pointer instead of the value |
| 91 | + // itself. |
| 92 | + alloca := builder.CreateAlloca(int64Type, "i64asptr") |
| 93 | + builder.CreateStore(operand, alloca) |
| 94 | + callParams = append(callParams, alloca) |
| 95 | + } else { |
| 96 | + // Unchanged parameter. |
| 97 | + callParams = append(callParams, operand) |
| 98 | + } |
| 99 | + } |
| 100 | + var callName string |
| 101 | + if returnType.TypeKind() != llvm.VoidTypeKind { |
| 102 | + // Only use the name of the old call instruction if the new |
| 103 | + // call is not a void call. |
| 104 | + // A call instruction with an i64 return type may have had a |
| 105 | + // name, but it cannot have a name after this transform |
| 106 | + // because the return type will now be void. |
| 107 | + callName = call.Name() |
| 108 | + } |
| 109 | + if fnType.ReturnType() == int64Type { |
| 110 | + // Pass a stack-allocated pointer as the first parameter |
| 111 | + // where the return value should be stored, instead of using |
| 112 | + // the regular return value. |
| 113 | + builder.CreateCall(externalFn, callParams, callName) |
| 114 | + returnValue := builder.CreateLoad(retvalAlloca, "retval") |
| 115 | + call.ReplaceAllUsesWith(returnValue) |
| 116 | + call.EraseFromParentAsInstruction() |
| 117 | + } else { |
| 118 | + newCall := builder.CreateCall(externalFn, callParams, callName) |
| 119 | + call.ReplaceAllUsesWith(newCall) |
| 120 | + call.EraseFromParentAsInstruction() |
| 121 | + } |
| 122 | + } |
| 123 | + } else { |
| 124 | + // The function has a definition in Go. This means that it may still |
| 125 | + // be called both Go and from external code. |
| 126 | + // Keep existing calls with the existing convention in place (for |
| 127 | + // better performance), but export a new wrapper function with the |
| 128 | + // correct calling convention. |
| 129 | + fn.SetLinkage(llvm.InternalLinkage) |
| 130 | + fn.SetUnnamedAddr(true) |
| 131 | + entryBlock := ctx.AddBasicBlock(externalFn, "entry") |
| 132 | + builder.SetInsertPointAtEnd(entryBlock) |
| 133 | + var callParams []llvm.Value |
| 134 | + if fnType.ReturnType() == int64Type { |
| 135 | + return errors.New("not yet implemented: exported function returns i64 with -wasm-abi=js; " + |
| 136 | + "see https://tinygo.org/compiler-internals/calling-convention/") |
| 137 | + } |
| 138 | + for i, origParam := range fn.Params() { |
| 139 | + paramValue := externalFn.Param(i) |
| 140 | + if origParam.Type() == int64Type { |
| 141 | + paramValue = builder.CreateLoad(paramValue, "i64") |
| 142 | + } |
| 143 | + callParams = append(callParams, paramValue) |
| 144 | + } |
| 145 | + retval := builder.CreateCall(fn, callParams, "") |
| 146 | + if retval.Type().TypeKind() == llvm.VoidTypeKind { |
| 147 | + builder.CreateRetVoid() |
| 148 | + } else { |
| 149 | + builder.CreateRet(retval) |
| 150 | + } |
| 151 | + } |
| 152 | + } |
| 153 | + |
| 154 | + return nil |
| 155 | +} |
0 commit comments