From 2e22d53e5d3446f3266074932a2df3ebc93f6cbf Mon Sep 17 00:00:00 2001 From: Ayke van Laethem Date: Sat, 3 Nov 2018 11:57:21 +0100 Subject: [PATCH] compiler: work around i64 limitation in JavaScript JavaScript does not support i64 directly, so make sure we pass a pointer instead which can be read from JavaScript. This is a temporary workaround which should be removed once JavaScript supports some form of i64 (probably in the form of BigInt). --- compiler/compiler.go | 93 ++++++++++++++++++++++++++++++++++++++++++++ docs/internals.rst | 14 +++++++ main.go | 9 +++++ 3 files changed, 116 insertions(+) diff --git a/compiler/compiler.go b/compiler/compiler.go index 1a29d5b8..915398d2 100644 --- a/compiler/compiler.go +++ b/compiler/compiler.go @@ -3561,6 +3561,99 @@ func (c *Compiler) NonConstGlobals() { } } +// 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. +// https://github.com/WebAssembly/design/issues/1172 +func (c *Compiler) ExternalInt64AsPtr() { + 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.") { + // Do not try to modify the signature of internal LLVM functions. + continue + } + hasInt64 := false + params := []llvm.Type{} + for param := fn.FirstParam(); !param.IsNil(); param = llvm.NextParam(param) { + if param.Type() == int64Type { + hasInt64 = true + params = append(params, int64PtrType) + } else { + params = append(params, param.Type()) + } + } + if !hasInt64 { + // No i64 in the paramter list. + continue + } + + // Add $i64param 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 + "$i64param") + fnType := fn.Type().ElementType() + externalFnType := llvm.FunctionType(fnType.ReturnType(), params, 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 $i64param 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{} + 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) + } + } + 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) + entryBlock := llvm.AddBasicBlock(externalFn, "entry") + c.builder.SetInsertPointAtEnd(entryBlock) + callParams := []llvm.Value{} + 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) + } + } + } +} + // Emit object file (.o). func (c *Compiler) EmitObject(path string) error { llvmBuf, err := c.machine.EmitToMemoryBuffer(c.mod, llvm.ObjectFile) diff --git a/docs/internals.rst b/docs/internals.rst index e401465a..2f5c916a 100644 --- a/docs/internals.rst +++ b/docs/internals.rst @@ -133,6 +133,20 @@ somewhat compatible with the C calling convention but with a few quirks: pointers. This avoids some overhead in the C calling convention and makes the work of the LLVM optimizers easier. + * The WebAssembly target never exports or imports a ``i64`` (``int64``, + ``uint64``) parameter. Instead, it replaces them with ``i64*``, allocating + the value on the stack. In other words, imported functions are called with a + 64-bit integer on the stack and exported functions must be called with a + pointer to a 64-bit integer somewhere in linear memory. + + This is a workaround for a limitation in JavaScript, which only deals with + doubles and can therefore only work with integers up to 32-bit in size (a + 64-bit integer cannot be represented exactly in a double, a 32-bit integer + can). It is expected that 64-bit integers will be `added in the near future + `_ at which point this + calling convention workaround may be removed. Also see `this wasm-bindgen + issue `_. + * Some functions have an extra context parameter appended at the end of the argument list. This only happens when both of these conditions hold: diff --git a/main.go b/main.go index 239f8eea..c1f2fd02 100644 --- a/main.go +++ b/main.go @@ -64,6 +64,15 @@ func Compile(pkgName, outpath string, spec *TargetSpec, config *BuildConfig, act return err } + // Browsers cannot handle external functions that have type i64 because it + // cannot be represented exactly in JavaScript (JS only has doubles). To + // keep functions interoperable, pass int64 types as pointers to + // stack-allocated values. + if strings.HasPrefix(spec.Triple, "wasm") { + c.ExternalInt64AsPtr() + c.Verify() + } + // Optimization levels here are roughly the same as Clang, but probably not // exactly. switch config.opt {