Hello, World! の話

  • 11
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

mixi Advent Calendar の18日目です。

はじめに

このエントリは「ハロー、Hello World!」という素晴らしい書籍読んで思いついたものです。実のところ、普段使用する言語においてHello, World!のような簡単なプログラムであってもどのように実行されているか本当に理解している人は少ないのではないでしょうか。なかなか調べる機会もありませんからね。せっかくなのでこの場を借りて調べてみようと思います。

内容に誤りがある場合は気軽にコメントで指摘してください。

なお、本エントリで参照する dmd/druntime/phobos の各ソースコードは Boost License 1.0 の基で配布されています。

目的

とりあえずこのあたりを追ってみます。

  • std.stdio はどこにあるのか
  • writeln の先では何が行われているのか
  • main の前に何が行われているのか

std.stdio はどこにあるのか

std.stdiophobos というD言語の標準ライブラリ郡の中にあるライブラリのひとつです。

http://dlang.org/phobos/index.html

http://dlang.org/phobos/std_stdio.html

dmdとphobosはどこで関連づけられているのでしょう?

dmdにはコンパイル中に何が行われているかを表すverboseオプション(-vで有効)があるので、これで少し中を覗いてみることにしましょう。

$ dmd -w -g -v helloworld.d
binary    dmd
version   v2.069.0
config    /etc/dmd.conf
parse     helloworld
...
gcc helloworld.o -o helloworld -g -m64 -L/usr/lib/x86_64-linux-gnu -Xlinker --export-dynamic -Xlinker -Bstatic -lphobos2 -Xlinker -Bdynamic -lpthread -lm -lrt -ldl

出力は少し長いので省略しています。多くはこのバイナリを生成するために必要となるモジュールのimportです。

さて、上のほうで config /etc/dmd.conf という行がありますね。このファイルは一体なんでしょうか?

;
; dmd.conf file for dmd
;
; dmd will look for dmd.conf in the following sequence of directories:
;   - current working directory
;   - directory specified by the HOME environment variable
;   - directory dmd resides in
;   - /etc directory
;
; Names enclosed by %% are searched for in the existing environment and inserted
;
; The special name %@P% is replaced with the path to this file
;

[Environment32]
DFLAGS=-I/usr/include/dmd/phobos -I/usr/include/dmd/druntime/import -L-L/usr/lib/i386-linux-gnu -L--export-dynamic

[Environment64]
DFLAGS=-I/usr/include/dmd/phobos -I/usr/include/dmd/druntime/import -L-L/usr/lib/x86_64-linux-gnu -L--export-dynamic

どうやらdmdはファイル定義からphobosやdruntimeの場所を参照してるようです。(実際に私の環境ではphobosはこの場所においてあります)

dmd.conf はmanもあるので、気になる人はそちらも参照してください。

Hello, World!

とにかく、D言語での「Hello, World!」のソースコードを見てみましょう。

import std.stdio;

void main()
{
  writeln("Hello, World!");
}

この種のプログラムで言語ごとの比較を論じるのは難しいものですが、CやJavaの構文と似ているんじゃないでしょうか。

writeln の前ではなにが行われているのか

writelnはstd.stdioで定義されている関数です。

stdioのbackendは windows/linux/OSX などOSによってかわります。

Linuxの場合はdmdの中で CRuntime_Glibc が定義されており、さらに GCC_IOstd.stdio で定義され、glibcの関数を使うようになっています。

実際にコードを追う前に、ltraceを使って雑に見てみましょう。

$ dmd -gc helloworld.d
$ ltrace -o dump ./helloworld

結果の一部を抜粋して、writeln と関係する部分を抜き出すとこのあたりになります。

...
fwide(0x7fad0e3b5400, 0)
flockfile(0x7fad0e3b5400, 0, 0x7fad0e3b5400, 0x66ddf0)
fwrite("Hello, World!", 1, 13, 0x7fad0e3b5400)
fputc_unlocked(10, 0x7fad0e3b5400, 13, 1024)
funlockfile(0x7fad0e3b5400, 0x7fad0ea00000, 0x7fad0ea00000, -1)
...

C言語の printf("Hello, World!\n") は標準出力への書き込みの部分で puts を呼びますが、D言語では fwrite を呼んでいることがわかります。

ここで奇妙な関数があることに気がつきます。 flockfile, funlockfile はいったいなんでしょうか?

flockfileのmanページをみると、この関数はstdioのFILEオブジェクトのロックを操作するための関数であり、他のスレッドが入出力操作の途中で割り込むことを避けるために使うためのもの、であるようです。ここでは fputc_unlocked のような関数がロックを行わないバージョンの標準入出力関数であることも同時に説明されています。

さて、ここからは実際にソースコードを読んでみることにしましょう。

phobosでのwriteln関数の定義はこのようになっています。

/***********************************
 * Equivalent to $(D write(args, '\n')).  Calling $(D writeln) without
 * arguments is valid and just prints a newline to the standard
 * output.
 */
void writeln(T...)(T args)
{
    import std.traits : isAggregateType;
    static if (T.length == 0)
    {
        import std.exception : enforce;

        enforce(fputc('\n', .trustedStdout._p.handle) == '\n', "fputc failed");
    }
    else static if (T.length == 1 &&
                    is(typeof(args[0]) : const(char)[]) &&
                    !is(typeof(args[0]) == enum) &&
                    !is(Unqual!(typeof(args[0])) == typeof(null)) &&
                    !isAggregateType!(typeof(args[0])))
    {
        import std.exception : enforce;

        // Specialization for strings - a very frequent case
        auto w = .trustedStdout.lockingTextWriter();

        static if (isStaticArray!(typeof(args[0])))
        {
            w.put(args[0][]);
        }
        else
        {
            w.put(args[0]);
        }
        w.put('\n');
    }
    else
    {
        // Most general instance
        trustedStdout.write(args, '\n');
    }
}

見慣れない単語はたくさんでてきました。

ネタばらしをしてしまうと、ここで else static if (T.length == 1 &&... という分岐に入ることになるわけなのですが、なぜそうなるのかさっぱりわからないですね!

ここは焦らず、順を追ってみていきましょう。

まず引数の数がひとつ (T.length == 1) であることはなんとなくわかります。

次に is(typeof(args[0]) : const(char)[]) ですが、少し厄介なかんじがしますね! typeof は任意の式の型を取得できる式です。今回のように typeof(文字列) である場合にstringという型情報を返してくれます。

次に is ですが、これは引数の型が意味的に正しいかどうかを判定するための式です。 is(typeof { exp...}) というのはis式が直接式を引数にとれないためにtypeofをはさむ、というハック的なものです。

http://www.codelogy.org/entry/2012/08/12/120835

次に ... : const(char)[]) という部分ですが、これは一体なんなのでしょうか? 実はこれもis式の特殊なパターンで、implicit conversion が可能であるかどうかのチェックを行うための構文となります。

http://wiki.dlang.org/Is_expression

次は !is(typeof(args[0]) == enum ですね。 enum は列挙体を表します。is(typeof({exp..}) == enum は名前付き列挙体の要素であるかをチェックします。 今回writelnに渡している文字列型はとくにそのようなことは行っていないため、この条件分岐は通ります。

!is(Unqual!(typeof(args[0])) == typeof(null)) をみてみましょう。 Unqual は修飾子(const, sharedなど)を取り除くtemplate構文です。ここでやっていることは null が来ている場合に弾くということだけですが、都度 cast(static)null のようなコードを書くのは面倒なところでUnqualを使うと楽に処理を書けます。

最後に !isAggregateType!(typeof(args[0]))) ですね。ここでAggregateTypeというのは構造体やクラスのような個々の要素としても集団としても参照可能であるデータの集まりを表す型を意味しています。今回は文字列が渡されているので、この条件分岐も通ります。

さて、これでようやくこの関数がどこへ向かうのかがわかるようになりました。

今回は trustedStdout.lockingTextWriter が肝となります。

/// Range primitive implementations.
void put(A)(A writeme)
    if (is(ElementType!A : const(dchar)) &&
        isInputRange!A &&
        !isInfinite!A)
{
    import std.exception : errnoEnforce;

    alias C = ElementEncodingType!A;
    static assert(!is(C == void));
    static if (isSomeString!A && C.sizeof == 1)
    {
        if (orientation_ <= 0)
        {
            //file.write(writeme); causes infinite recursion!!!
            //file.rawWrite(writeme);
            static auto trustedFwrite(in void* ptr, size_t size, size_t nmemb, FILE* stream) @trusted
            {
                return .fwrite(ptr, size, nmemb, stream);
            }
            auto result =
                trustedFwrite(writeme.ptr, C.sizeof, writeme.length, fps_);
            if (result != writeme.length) errnoEnforce(0);
            return;
        }
    }

    // put each character in turn
    foreach (dchar c; writeme)
    {
        put(c);
    }
}

ここでようやく fwrite が出てきました。ここでfwriteを特別扱いしたのは文字列を都度putc_unlockedを実行するコストを避けたいからでしょう。

main の前になにが行われているか

D言語のmain関数はC言語のmain関数と異なるものです。

( ՞ਊ ՞) :~/dev/dlang/helloworld $ dmd -g helloworld.d
( ՞ਊ ՞) :~/dev/dlang/helloworld $ gdb ./helloworld
...
(gdb) b _Dmain
Breakpoint 1 at 0x4351a4: file helloworld.d, line 5.
(gdb) r
...
Breakpoint 1, D main () at helloworld.d:5
5         writeln("Hello, World!");
(gdb) bt
#0  D main () at helloworld.d:5
#1  0x0000000000436227 in rt.dmain2._d_run_main() ()
#2  0x000000000043617d in rt.dmain2._d_run_main() ()
#3  0x00000000004361e3 in rt.dmain2._d_run_main() ()
#4  0x000000000043617d in rt.dmain2._d_run_main() ()
#5  0x00000000004360da in _d_run_main ()
#6  0x0000000000435678 in main ()

_Dmainのアセンブリ表現もみておきましょう。

(gdb) disas
Dump of assembler code for function _Dmain:
   0x00000000004351a0 <+0>:     push   %rbp
   0x00000000004351a1 <+1>:     mov    %rsp,%rbp
=> 0x00000000004351a4 <+4>:     mov    $0x467940,%edx
   0x00000000004351a9 <+9>:     mov    $0xd,%edi
   0x00000000004351ae <+14>:    mov    %rdx,%rsi
   0x00000000004351b1 <+17>:    callq  0x4351c0 <_D3std5stdio16__T7writelnTAyaZ7writelnFNfAyaZv>
   0x00000000004351b6 <+22>:    xor    %eax,%eax
   0x00000000004351b8 <+24>:    pop    %rbp
   0x00000000004351b9 <+25>:    retq

ソースコード的にとっかかりになりそうなところはこのへんでしょうか。

  • dmd:src/mars.d
extern (C++) void genCmain(Scope* sc)
{
    if (entrypoint)
        return;
    /* The D code to be generated is provided as D source code in the form of a string.
     * Note that Solaris, for unknown reasons, requires both a main() and an _main()
     */
    static __gshared const(char)* cmaincode =
    q{
        extern(C)
        {
            int _d_run_main(int argc, char **argv, void* mainFunc);
            int _Dmain(char[][] args);
            int main(int argc, char **argv)
            {
                return _d_run_main(argc, argv, &_Dmain);
            }
            version (Solaris) int _main(int argc, char** argv) { return main(argc, argv); }
        }
    };
...

callstack的には

main -> _d_run_main(druntime) -> ...

となっていますね、gdbのbacktrace表示とあわせてみても正しいよう見えます。

ここで _d_run_main はdruntimeのほうにもコードがあります。

druntime:rt/dmain2.d

extern (C) int _d_run_main(int argc, char **argv, MainFunc mainFunc)
{
...
    void runAll()
    {
        if (rt_init() && runModuleUnitTests())
            tryExec({ result = mainFunc(args); });
        else
            result = EXIT_FAILURE;

        if (!rt_term())
            result = (result == EXIT_SUCCESS) ? EXIT_FAILURE : result;
    }

    tryExec(&runAll);
...
}

tryExecは渡されるdelegateを実行して例外が起きた時のハンドリングをしているだけなのでここでは省略しています。

runAll の中を見てみましょう。

まず rt_initrunModuleUnitTests 関数がそれぞれ実行され、それらの実行が成功であったときに mainFunc(args) すなわち _Dmain を実行する流れとなっています。

rt_init はmutex属性オブジェクトやGC、モジュール内のコンストラクタの初期化をしています。

/**********************************************
 * Initialize druntime.
 * If a C program wishes to call D code, and there's no D main(), then it
 * must call rt_init() and rt_term().
 */
extern (C) int rt_init()
{
...
    _d_monitor_staticctor();
    _d_critical_init();

    try
    {
        initSections();
        // this initializes mono time before anything else to allow usage
        // in other druntime systems.
        _d_initMonoTime();
        gc_init();
        initStaticDataGC();
        lifetime_init();
        rt_moduleCtor();
        rt_moduleTlsCtor();
        return 1;
    }
...
}

続いて runModuleUnitTests はimportしているモジュールのunittestを実行しています。

プログラムの実行前にユニットテストが実行されるというのはなかなか驚きですね。

/**
 * This routine is called by the runtime to run module unit tests on startup.
 * The user-supplied unit tester will be called if one has been supplied,
 * otherwise all unit tests will be run in sequence.
 *
 * Returns:
 *  true if execution should continue after testing is complete and false if
 *  not.  Default behavior is to return true.
 */
extern (C) bool runModuleUnitTests()
{
    // backtrace
    version( CRuntime_Glibc )
        import core.sys.linux.execinfo;
...
    static if( __traits( compiles, backtrace ) )
    {
        import core.sys.posix.signal; // segv handler

        static extern (C) void unittestSegvHandler( int signum, siginfo_t* info, void* ptr ) nothrow
        {
            static enum MAXFRAMES = 128;
            void*[MAXFRAMES]  callstack;
            int               numframes;

            numframes = backtrace( callstack.ptr, MAXFRAMES );
            backtrace_symbols_fd( callstack.ptr, numframes, 2 );
        }

        sigaction_t action = void;
        sigaction_t oldseg = void;
        sigaction_t oldbus = void;

        (cast(byte*) &action)[0 .. action.sizeof] = 0;
        sigfillset( &action.sa_mask ); // block other signals
        action.sa_flags = SA_SIGINFO | SA_RESETHAND;
        action.sa_sigaction = &unittestSegvHandler;
        sigaction( SIGSEGV, &action, &oldseg );
        sigaction( SIGBUS, &action, &oldbus );
        scope( exit )
        {
            sigaction( SIGSEGV, &oldseg, null );
            sigaction( SIGBUS, &oldbus, null );
        }
    }

    if( Runtime.sm_moduleUnitTester is null )
    {
        size_t failed = 0;
        foreach( m; ModuleInfo )
        {
            if( m )
            {
                auto fp = m.unitTest;

                if( fp )
                {
                    try
                    {
                        fp();
                    }
                    catch( Throwable e )
                    {
                        _d_print_throwable(e);
                        failed++;
                    }
                }
            }
        }
        return failed == 0;
    }
    return Runtime.sm_moduleUnitTester();
}

これらの処理が問題なく行われた後にmain関数の中身が実行されるというわけです。

実行環境

最後に、私が本記事の内容を検証した際の実行環境を掲載しておきます。

  • OS/Distribution
$ uname -mrv
3.13.0-68-generic #111-Ubuntu SMP Fri Nov 6 18:17:06 UTC 2015 x86_64
$ cat /etc/issue
Ubuntu 14.04.3 LTS \n \l
  • D言語

コンパイラとしてDMDを使います。

$ dmd --version
DMD64 D Compiler v2.069.0
Copyright (c) 1999-2015 by Digital Mars written by Walter Bright

明日

明日は GitHub ID: oppai の人 さんこと @kodam実践ぼっち駆動開発 です。