レポジトリはこちらにおいています: https://github.com/kubo39/zephyr_ldc_hello
なので(?)細かい解説はあまりしません。
環境構築
Zephyrの環境構築は https://docs.zephyrproject.org/latest/develop/getting_started/index.html みれば問題なくいけると思います。
D言語のコンパイラで今回使ってるLDCは https://dlang.org/download.html とかをみて適当に入れてください。
Qemuで動かす
今回は qemu_cortex_m3
で動かすというのをまず目標におきました。
そのためLDCのtargetは thumbv7em-none-linux-musl-gnueabi
にしています。
またnewlibの関数を使いたかったのでprj.confで CONFIG_NEWLIB_LIBC=y
を定義します。
newlibを使っているのにtarget tripleでmuslを指定しているのはなんで?という部分は後述します。
あとはまあなんやかんや指定してCMakeからExternalProjectとして定義して呼び出す形にし、いい感じに配置してやると動きます。
$ cd $ZEPHYR_BASE
$ west build -b qemu_cortex_m3 samples/ldc_hello
$ west build -t run
(...)
Hello from 'LDC'!
assertion "array index out of bounds" failed: file "d_src/hello.d", line 29, function: hello.d_main
exit
D言語のコード
コードはこんな感じになりました。
import ldc.attributes : cold;
@nogc:
nothrow:
// newlib
extern (C)
{
pragma(printf)
int printf(scope const char* fmt, ...);
noreturn __assert_func(scope const char* file, int line, scope const char* func, scope const char* failedexpr);
}
// Wrapping newlib's __assert_func.
//
// LDC: https://github.com/ldc-developers/ldc/blob/9976807e0e1acf24edfb4ba35d28c19a3f0227f2/gen/runtime.cpp#L367
// void __assert(const char *msg, const char *file, unsigned line)
// newlib: https://github.com/bminor/newlib/blob/80cda9bbda04a1e9e3bee5eadf99061ed69ca5fb/newlib/libc/stdlib/assert.c#L68-L70
// void __assert(const char *file, int line, const char *failedexpr)
private extern (C) @cold noreturn __assert_fail(const(char)* msg, const(char)* file, int line, const(char)* func)
{
__assert_func(file, line, func, msg);
}
extern (C) noreturn d_main()
{
string ldc = "LDC";
printf("Hello from '%.*s'!\n", cast(int)ldc.length, ldc.ptr);
int[2] arr;
int x;
foreach (i; 0..3)
x = arr[i]; // assertion "array index out of bounds" failed!
while (true) {}
}
コード本体の解説はしませんが、ここで使っているD言語の機能としていくつかあげておきます:
- noreturn: 返り値をもたない関数はbottom型にしています: https://dlang.org/spec/type.html#noreturn
- pragma(printf): printf-styleの関数で書式チェックをしています: https://dlang.org/spec/pragma.html#printf
- nogc: 関数内でGCによるメモリ管理が発生しないことを保証しています: https://dlang.org/spec/function.html#nogc-functions
- nothrow: 関数が例外を投げないことを保証しています: https://dlang.org/spec/function.html#nothrow-functions
- extern(C): Cの関数を直接呼ぶことができます: https://dlang.org/spec/interfaceToC.html#calling_c_functions
musl target と __assert
D言語のコンパイラは配列の境界チェックをサポートしています。
その際に--checkactionオプションで範囲外アクセスが起きた時の挙動をhalt/C/D/contextから選択できるのですが、
ここで --checkaction=C
を選択した場合、範囲外アクセスが起きた場合はC言語のassert関数を呼び出す処理になっています。
ただし正確にはassert関数を直接呼ぶのではなく、各プラットフォーム・アーキテクチャごとに異なる定義になっているassert相当の関数を呼んでいます。
コンパイラでいうと以下の箇所がその処理の一例です。
- ここでは関数名をそれぞれの環境で最適なものを選択するようにしています: https://github.com/ldc-developers/ldc/blob/9976807e0e1acf24edfb4ba35d28c19a3f0227f2/gen/runtime.cpp#L356-L381
// C assert function:
// OSX: void __assert_rtn(const char *func, const char *file, unsigned line,
// const char *msg)
// Android: void __assert(const char *file, int line, const char *msg)
// MSVC: void _assert(const char *msg, const char *file, unsigned line)
// Solaris: void __assert_c99(const char *assertion, const char *filename, int line_num,
// const char *funcname);
// Musl: void __assert_fail(const char *assertion, const char *filename, int line_num,
// const char *funcname);
// uClibc: void __assert(const char *assertion, const char *filename, int linenumber,
// const char *function);
// else: void __assert(const char *msg, const char *file, unsigned line)
static const char *getCAssertFunctionName() {
const auto &triple = *global.params.targetTriple;
if (triple.isOSDarwin()) {
return "__assert_rtn";
} else if (triple.isWindowsMSVCEnvironment()) {
return "_assert";
} else if (triple.isOSSolaris()) {
return "__assert_c99";
} else if (triple.isMusl()) {
return "__assert_fail";
}
return "__assert";
}
- こちらはCのassert関数の生成と引数の順序の対応を行っている処理です: https://github.com/ldc-developers/ldc/blob/81e78a5ec882e842de779c4b5197bd7b539d6f13/gen/llvmhelpers.cpp#L291-L329
void DtoCAssert(Module *M, const Loc &loc, LLValue *msg) {
const auto &triple = *global.params.targetTriple;
const auto file =
DtoConstCString(loc.filename ? loc.filename : M->srcfile.toChars());
const auto line = DtoConstUint(loc.linnum);
const auto fn = getCAssertFunction(loc, gIR->module);
llvm::SmallVector<LLValue *, 4> args;
if (triple.isOSDarwin()) {
const auto irFunc = gIR->func();
const auto funcName =
irFunc && irFunc->decl ? irFunc->decl->toPrettyChars() : "";
args.push_back(DtoConstCString(funcName));
args.push_back(file);
args.push_back(line);
args.push_back(msg);
} else if (triple.isOSSolaris() || triple.isMusl() ||
global.params.isUClibcEnvironment) {
const auto irFunc = gIR->func();
const auto funcName =
(irFunc && irFunc->decl) ? irFunc->decl->toPrettyChars() : "";
args.push_back(msg);
args.push_back(file);
args.push_back(line);
args.push_back(DtoConstCString(funcName));
} else if (triple.getEnvironment() == llvm::Triple::Android) {
args.push_back(file);
args.push_back(line);
args.push_back(msg);
} else {
args.push_back(msg);
args.push_back(file);
args.push_back(line);
}
gIR->CreateCallOrInvoke(fn, args);
gIR->ir->CreateUnreachable();
}
ところが、ここで一点問題がありました。
LDCにおけるglibc/muslでないLinux環境のassert関数とnewlibのassert関数では引数の対応順序が異なっています。
// LDC: https://github.com/ldc-developers/ldc/blob/9976807e0e1acf24edfb4ba35d28c19a3f0227f2/gen/runtime.cpp#L367
// void __assert(const char *msg, const char *file, unsigned line)
// newlib: https://github.com/bminor/newlib/blob/80cda9bbda04a1e9e3bee5eadf99061ed69ca5fb/newlib/libc/stdlib/assert.c#L68-L70
// void __assert(const char *file, int line, const char *failedexpr)
そのため、単純にこのまま呼んでしまうと一見不可解なエラーメッセージが表示されてしまいます。
$ west build -t run
(...)
Hello from 'LDC'!
assertion "" failed: file "array index out of bounds", line 40362
本来であればコンパイラ側に手を入れたいところですが、現状LLVMはtarget tripleでnewlibを対応していません。
LDCも現時点ではハンドリングをあきらめています: https://github.com/ldc-developers/ldc/blob/master/driver/main.cpp#L625-L628 (追記: ふつうにできたのでパッチ書いた https://github.com/ldc-developers/ldc/pull/4351)
そこで今回はtargetをMuslにして、 __assert_fail
という関数を定義してnewlibの __assert_func
関数をラップして引数の対応が期待通りになるような修正を行っています。
ここでmuslを使う理由ですが、 __assert
の多重定義を避けることが可能なもののうち、最も環境的に影響が小さいと考えられるものとして選択しました。
コンパイラはmuslターゲットの場合は __assert_fail
を呼び出そうとしますが、newlibは当然そんなものは持っていないので自前で定義したほうを呼び出します。
// Wrapping newlib's __assert_func.
//
// LDC: https://github.com/ldc-developers/ldc/blob/9976807e0e1acf24edfb4ba35d28c19a3f0227f2/gen/runtime.cpp#L367
// void __assert(const char *msg, const char *file, unsigned line)
// newlib: https://github.com/bminor/newlib/blob/80cda9bbda04a1e9e3bee5eadf99061ed69ca5fb/newlib/libc/stdlib/assert.c#L68-L70
// void __assert(const char *file, int line, const char *failedexpr)
private extern (C) @cold noreturn __assert_fail(const(char)* msg, const(char)* file, int line, const(char)* func)
{
__assert_func(file, line, func, msg);
}
将来的にはLLVMのnewlib対応を待ってコンパイラ側に手を入れることができるとよいなあと思っています。