ぶていのログでぶログ

思い出したが吉日

Zigを使ってWindows向けにmrubyをクロスビルドしたかったが失敗した

rfではLinux/Mac/Windows向けのバイナリを提供している。 LinuxとMac向けのバイナリについてはZigを使ってクロスビルドしている。 では、Windows向けはどうしているかというとgccを使ってクロスビルドしている。 何故かというと、rfのWindows対応を開発していた当初もZigを使ったクロスビルドを試していていたのだが、うまくいかなかった。 そのままWindows対応を見送るのはなんとなく嫌だったので、gccを使った方法に舵を切ったという経緯がある。

最近rfのビルド環境を見直していて、ついでにWindows向けのバイナリもZigを使う方法がないか再度調査していた。 結果としてはビルドはできたが意図した通り動かず、そしてそれがどうすればいいかわからなかったので失敗に終わった…。 失敗はしたがまたチャレンジした時の参考にするために備忘として残しておく。

なぜZigでビルドできないか

mrubyのdefaut.gemboxに含まれるmruby-socket*1がgai_strerrorA/gai_strerrorW*2が定義されていないためにエラーになる。 この関数はMSDNのドキュメントにも載っているのでWindows上には存在しているのだが、ws2_32.dllのリンクを追加しても関数名が解決できなくてエラーになる。 種明かしをすると、この関数はインラインで定義されていてws2_32.dllには定義されていないのであった。 そのため、これらの関数を自分で定義することでこのエラーを回避することができる。

gai_strerrorを定義してビルドする

今回は以下のようなファイルを作成した。

/* crossbuild/gai_strerror.c */
#include <winsock2.h>
#include <ws2tcpip.h>

#ifndef UNICODE
char* gai_strerrorA(int errcode)
{
    static char buf[GAI_STRERROR_BUFFER_SIZE + 1];

    FormatMessageA(
        FORMAT_MESSAGE_FROM_SYSTEM
            | FORMAT_MESSAGE_IGNORE_INSERTS
            | FORMAT_MESSAGE_MAX_WIDTH_MASK,
        NULL,
        errcode,
        MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
        buf, GAI_STRERROR_BUFFER_SIZE + 1,
        NULL);

    return buf;
}

#else

WCHAR* gai_strerrorW(int errcode)
{
    static WCHAR buf[GAI_STRERROR_BUFFER_SIZE + 1];

    FormatMessageW(
        FORMAT_MESSAGE_FROM_SYSTEM
            | FORMAT_MESSAGE_IGNORE_INSERTS
            | FORMAT_MESSAGE_MAX_WIDTH_MASK,
        NULL,
        errcode,
        MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
        buf, GAI_STRERROR_BUFFER_SIZE + 1,
        NULL);

    return buf;
}

#endif

次にbuild_config.rbを以下のように定義した。

def gem_config(conf)
  conf.gembox 'default'
end

def build_config(conf, target = nil)
  [conf.cc, conf.linker].each do |cc|
    cc.command = "zig cc"
    cc.flags += ['-target', target] if target
  end

  conf.archiver.command = 'zig ar'
  conf.host_target = target if target
end

MRuby::Build.new do |conf|
  build_config(conf)
  gem_config(conf)
end

MRuby::CrossBuild.new('windows-amd64') do |conf|
  build_config(conf, 'x86_64-windows')

  conf.exts do |exts|
    exts.object = '.obj'
    exts.executable = '.exe'
  end

  build_root = File.join(build_dir, 'crossbuild')
  FileUtils.mkdir_p(build_root)
  `#{conf.cc.command} #{conf.cc.flags.join(' ')} -o #{build_root}/gai_strerror.o -c #{__dir__}/crossbuild/gai_strerror.c`

  conf.linker.flags << "#{build_root}/gai_strerror.o"
  conf.host_target = "x86_64-w64-mingw32"  # required for `for_windows?` used by `mruby-socket` gem

  gem_config(conf)
end

build_config.rbでは任意のCファイルをビルド出来ないので無理やりコマンドを実行してビルドしている。 最後に以下のコマンドでビルドする。

$ MRUBY_CONFIG=$(pwd)/build_config.rb MRUBY_BUILD_DIR=$(pwd)/build rake -f /path/to/mruby/Rakefile
-- snip --
================================================
      Config Name: windows-amd64
 Output Directory: build/windows-amd64
         Binaries: mrbc
    Included Gems:
-- snip --

ちなみに、ここまでしなくともmruby-socketを使わないのであれば、インストールするGemから外してあげれば普通にビルドはできる。

実行してみる

普通に実行できる。

> .\mruby.exe --version
mruby 3.2.0 (2023-02-24)

> .\mruby.exe -e 'puts MRUBY_VERSION'
3.2.0

だが、標準エラーに出力しようとするなぜか出力されない…

> .\mruby.exe -e 'warn MRUBY_VERSION'

>

しかもMicrosoft DefenderによってTorojan判定されてしまう。。なぜぇ。。。

おわりに

Zigを使って再度Windows向けにクロスビルドしようとしたが失敗に終わった。。 「zig cc Windows corss build」とかで検索してもあまり情報がでてこないので難しい。 また、zigの内部にはllvmが入っているので「llvm mingw crossbuild」とかで検索をかけてもあまり情報がでてこないので手詰まりになってしまった。

一旦はWindows向けはgccでのビルドに戻して、何かのタイミングでZigのことを思い出したら再度挑戦したいと思う。

参考

*1:正確にはdefault.gemboxがrequireしているstdio.gemboxに含まれている

*2:gai_strerror関数の実体。WindowsにおいてはANSI環境とUnicode環境で関数の挙動を変更していることがあり関数名のサフィックスにA/Wをつけることで区別している