kubo39's blog

ただの雑記です。

LDCでwasmのmultivalue拡張できないか軽く試す

LDCでWebAssemblyのmultivalue拡張を試したい。

コンパイラはLDC1.25.1でまず試してみる。

以下のようなコードを書いてみる。

extern (C):
nothrow:
@nogc:

struct TwoI32
{
    int aa;
    int bb;
}

export TwoI32 return_two_i32()
{
    return TwoI32(1, 2);
}
$ ldc2 -mtriple=wasm32-unknown-unknown \
 -mattr=+multivalue \
 -betterC \
 -fvisibility=hidden \
 --frame-pointer=none \
 -L=-no-entry \
 multivalue.d

生成したwasmをwat形式にするとこのようになり、+multivalueが効いていないことがわかる。

(module
  (type (;0;) (func (param i32)))
  (func $return_two_i32 (type 0) (param i32)
    (local i32 i32)
    i32.const 2
    local.set 1
    i32.const 1
    local.set 2
    local.get 0
    local.get 2
    i32.store
    local.get 0
    local.get 1
    i32.store offset=4
    return)
  (table (;0;) 1 1 funcref)
  (memory (;0;) 2)
  (global (;0;) (mut i32) (i32.const 66560))
  (export "memory" (memory 0))
  (export "return_two_i32" (func $return_two_i32)))

どのような流れでコンパイルされているかを知るために、verboseフラグをたててみる。 LDCで詳しくみたい場合は-vvをつける。

$ ldc2 -mtriple=wasm32-unknown-unknown \
 -mattr=+multivalue -betterC -fvisibility=hidden \
 --frame-pointer=none -L=-no-entry \
 -vv multivalue.d
Targeting 'wasm32-unknown-unknown' (CPU 'generic' with features '+multivalue')
WARNING: Unknown ABI, guessing...
Building type: real
Building type: uint
Building type: int
CodeGenerator::emit(multivalue)
* resetting 3848 Dsymbols
* *** Initializing D runtime declarations ***
* * building runtime module
* Import::codegen for multivalue.object
* StructDeclaration::codegen: 'multivalue.TwoI32'
* * Resolving struct type: TwoI32 (multivalue.d(5))
* * * Building type: TwoI32
* * * * Building struct type multivalue.TwoI32 @ multivalue.d(5)
* * * * * Field priority for aa: 1
* * * * * Field priority for bb: 1
* * * * * final struct type: %multivalue.TwoI32 = type { i32, i32 }
* * VarDeclaration::codegen(): 'multivalue.TwoI32.aa'
* * * DtoResolveVariable(multivalue.TwoI32.aa)
* * VarDeclaration::codegen(): 'multivalue.TwoI32.bb'
* * * DtoResolveVariable(multivalue.TwoI32.bb)
* DtoDefineFunction(multivalue.return_two_i32): multivalue.d(11)
* * DtoFunctionType(nothrow @nogc extern (C) TwoI32())
* * * Building type: TwoI32*
* * * Building type: void
* * * Final function type: void (%multivalue.TwoI32*)
* * DtoResolveFunction(multivalue.return_two_i32): multivalue.d(11)
* * DtoDeclareFunction(multivalue.return_two_i32): multivalue.d(11)
* * * DtoFunctionType(nothrow @nogc extern (C) TwoI32())
* * * func = declare void @return_two_i32(%multivalue.TwoI32*)

DtoDefineFunctionまわりが怪しそう。

  • Final function type: void (%multivalue.TwoI32*)
    • irFty.ret->ltypeがvoidになってそう
    • ここはTwoI32がきてほしい

そろそろコードをみてみる。

とりあえず上のほうをみると、TargetABIというアーキテクチャごとのABIにかんする情報をもってそうなものがある。

llvm::FunctionType *DtoFunctionType(Type *type, IrFuncTy &irFty, Type *thistype,
                                    Type *nesttype, FuncDeclaration *fd) {
  IF_LOG Logger::println("DtoFunctionType(%s)", type->toChars());
  LOG_SCOPE

  // sanity check
  assert(type->ty == Tfunction);
  TypeFunction *f = static_cast<TypeFunction *>(type);
  assert(f->next && "Encountered function type with invalid return type; "
                    "trying to codegen function ignored by the frontend?");

  // Return cached type if available
  if (irFty.funcType) {
    return irFty.funcType;
  }

  TargetABI *abi = fd && DtoIsIntrinsic(fd) ? TargetABI::getIntrinsic() : gABI;
(...)

LDCのコード(2021/04/17最終更新 コミットハッシュ:e6deb6c61f087553b58028ccab41c20c729a6b90)をみるとwasm32ではABI判定がされないのでUnknownTargetABIになる。

TargetABI *TargetABI::getTarget() {
  switch (global.params.targetTriple->getArch()) {
  case llvm::Triple::x86:
    return getX86TargetABI();
  case llvm::Triple::x86_64:
    if (global.params.targetTriple->isOSWindows()) {
      return getWin64TargetABI();
    } else {
      return getX86_64TargetABI();
    }
  case llvm::Triple::mips:
  case llvm::Triple::mipsel:
  case llvm::Triple::mips64:
  case llvm::Triple::mips64el:
    return getMIPS64TargetABI(global.params.is64bit);
  case llvm::Triple::ppc:
  case llvm::Triple::ppc64:
    return getPPCTargetABI(global.params.targetTriple->isArch64Bit());
  case llvm::Triple::ppc64le:
    return getPPC64LETargetABI();
  case llvm::Triple::aarch64:
  case llvm::Triple::aarch64_be:
    return getAArch64TargetABI();
  case llvm::Triple::arm:
  case llvm::Triple::armeb:
  case llvm::Triple::thumb:
  case llvm::Triple::thumbeb:
    return getArmTargetABI();
  default:
    Logger::cout() << "WARNING: Unknown ABI, guessing...\n";
    return new UnknownTargetABI;
  }
}

UnknownTargetABIは以下のような構造になっている。

// Some reasonable defaults for when we don't know what ABI to use.
struct UnknownTargetABI : TargetABI {
  bool returnInArg(TypeFunction *tf, bool) override {
    if (tf->isref()) {
      return false;
    }

    // Return structs and static arrays on the stack. The latter is needed
    // because otherwise LLVM tries to actually return the array in a number
    // of physical registers, which leads, depending on the target, to
    // either horrendous codegen or backend crashes.
    Type *rt = tf->next->toBasetype();
    return passByVal(tf, rt);
  }

  bool passByVal(TypeFunction *, Type *t) override {
    return DtoIsInMemoryOnly(t);
  }

  void rewriteFunctionType(IrFuncTy &) override {
    // why?
  }
};
bool DtoIsInMemoryOnly(Type *type) {
  Type *typ = type->toBasetype();
  TY t = typ->ty;
  return (t == Tstruct || t == Tsarray);
}

このままだとこういうLLVM IRを吐く。

ldc2 -mtriple=wasm32-unknown-unknown \
 -mattr=+multivalue \
 -betterC \
 -fvisibility=hidden \
 --frame-pointer=none \
 --output-ll \
 -L=-no-entry\
 multivalue.d
(...)
; [#uses = 0]
define void @return_two_i32(%multivalue.TwoI32* noalias sret align 4 %.sret_arg) #0 {
  %1 = getelementptr inbounds %multivalue.TwoI32, %multivalue.TwoI32* %.sret_arg, i32 0, i32 0 ; [#uses = 1, type = i32*]
  store i32 1, i32* %1, align 4
  %2 = getelementptr inbounds %multivalue.TwoI32, %multivalue.TwoI32* %.sret_arg, i32 0, i32 1 ; [#uses = 1, type = i32*]
  store i32 2, i32* %2, align 4
  ret void
}

attributes #0 = { "frame-pointer"="none" "target-cpu"="generic" "target-features"="+multivalue" }

というわけで、これを修正する必要がある。

継承関係とかみるとこのへんが呼ばれるらしい。

  • bool returnInArg(TypeFunction *tf, bool)
  • bool passByVal(TypeFunction , Type t)
  • void rewriteFunctionType(IrFuncTy &)

関数の返り値がvoidになっているのがまずおかしい。 強引に修正するなら、とりあえず以下のようにやるとTwoI32型の返り値にはなる。

bool returnInArg(TypeFunction *tf, bool) override {
    return false;
}

これで生成されるLLVM IRはTwoI32を返すようにはなる。

; ModuleID = 'multivalue.d'
source_filename = "multivalue.d"
target datalayout = "e-m:e-p:32:32-i64:64-n32:64-S128"
target triple = "wasm32-unknown-unknown"

%multivalue.TwoI32 = type { i32, i32 }

; [#uses = 0]
define %multivalue.TwoI32 @return_two_i32() #0 {
  %.structliteral = alloca %multivalue.TwoI32, align 4 ; [#uses = 3, size/byte = 8]
  %1 = getelementptr inbounds %multivalue.TwoI32, %multivalue.TwoI32* %.structliteral, i32 0, i32 0 ; [#uses = 1, type = i32*]
  store i32 1, i32* %1, align 4
  %2 = getelementptr inbounds %multivalue.TwoI32, %multivalue.TwoI32* %.structliteral, i32 0, i32 1 ; [#uses = 1, type = i32*]
  store i32 2, i32* %2, align 4
  %3 = load %multivalue.TwoI32, %multivalue.TwoI32* %.structliteral, align 4 ; [#uses = 1]
  ret %multivalue.TwoI32 %3
}

attributes #0 = { "frame-pointer"="none" "target-cpu"="generic" "target-features"="+multivalue" }

!llvm.ident = !{!0}

!0 = !{!"ldc version 1.26.0-git-e6deb6c-dirty"}

ただし今度は生成されるwasmがinvalidになってしまう。

$ wasm2wat multivalue.wasm
000000e: error: invalid function result count 2, only 1 bytes left in section

ひとまずここまで。続くかもしれないし続かないかもしれない。