Help us understand the problem. What is going on with this article?

Haxe + HashLink (HL/C) でコールバック処理はどうコンパイルされるのか

前置き

概要

タイトルの件で、書き方や条件次第でパフォーマンスが変わったりしないかというのを確認したかった。

単なる自分向けの備忘録というか記録の羅列です。
コンパイル時の規則もC言語の読み方もよく分かっていません。
たぶん遠回りしてるんだろうなという感じがひしひしといたします。

環境

Haxe 4.0.5
HashLink 1.10

GitHub

https://github.com/fal-works/hashlink-test/tree/master/callback

プリミティブ値を対象とする処理

(準備)コールバック関数を受け取る関数

単体実行

任意のコールバック関数を受け取って実行する関数を用意

IntCaller.hx(抜粋)
class IntCaller {
    public static function call(value:Int, callback:Int->Int)
        return callback(value);
}

結果

IntCaller.c(抜粋)
int IntCaller_call(int r0,vclosure* r1) {
    int r2;
    if( r1 == NULL ) hl_null_access();
    r2 = r1->hasValue ? ((int (*)(vdynamic*,int))r1->fun)((vdynamic*)r1->value,r0) : ((int (*)(int))r1->fun)(r0);
    return r2;
}
  • 関数はぜんぶクロージャ扱いになるらしい
  • nullチェックと、紐づくデータ(たぶんクロージャの環境とかインスタンスとか)の有無のチェックをしている
  • staticメソッドのときに hasValue が 0 とのこと
    (参考: C API Documentation - HaxeFoundation/hashlink Wiki
  • 静的に解決するのは難しい or メリットが少ないということかしら

ループ実行

コールバック関数をfor文の中で実行する関数を用意

IntCaller.hx(抜粋)
class IntCaller {
    public static function repeatCall(initialValue:Int, callback:Int->Int, repetition:Int) {
        var result = initialValue;
        for (_ in 0...repetition)
            result = callback(result);

        return result;
    }
}

結果

IntCaller.c(抜粋)
int IntCaller_repeatCall(int r0,vclosure* r1,int r2) {
    int r3, r4, r5, r6;
    r3 = r0;
    r4 = 0;
    r5 = r2;
    label$84d70b4_2_3:
    if( r4 >= r5 ) goto label$84d70b4_2_10;
    ++r4;
    if( r1 == NULL ) hl_null_access();
    r6 = r1->hasValue ? ((int (*)(vdynamic*,int))r1->fun)((vdynamic*)r1->value,r3) : ((int (*)(int))r1->fun)(r3);
    r3 = r6;
    goto label$84d70b4_2_3;
    label$84d70b4_2_10:
    return r3;
}

無駄なチェックに見えるけど後でCコンパイラに最適化される?

配列ループ実行

forEach的なやつを用意

IntCaller.hx(抜粋)
class IntCaller {
    public static function forEach(array:Array<Int>, callback:Int->Void)
        for (i in 0...array.length)
            callback(array[i]);
}


結果
IntCaller.hx(抜粋)
void IntCaller_forEach(hl__types__ArrayBytes_Int r0,vclosure* r1) {
    vbyte *r8;
    int r2, r4, r5, r6, r7;
    r2 = 0;
    if( r0 == NULL ) hl_null_access();
    r4 = r0->length;
    label$84d70b4_4_3:
    if( r2 >= r4 ) goto label$84d70b4_4_19;
    r5 = r2;
    ++r2;
    if( r1 == NULL ) hl_null_access();
    if( r0 == NULL ) hl_null_access();
    r7 = r0->length;
    if( ((unsigned)r5) < ((unsigned)r7) ) goto label$84d70b4_4_13;
    r6 = 0;
    goto label$84d70b4_4_17;
    label$84d70b4_4_13:
    r8 = r0->bytes;
    r7 = 2;
    r7 = r5 << r7;
    r6 = *(int*)(r8 + r7);
    label$84d70b4_4_17:
    r1->hasValue ? ((void (*)(vdynamic*,int))r1->fun)((vdynamic*)r1->value,r6) : ((void (*)(int))r1->fun)(r6);
    goto label$84d70b4_4_3;
    label$84d70b4_4_19:
    return;
}

特記事項なし

単体実行(inline)

call() と同じやつのinline版も一応作っておく

IntCaller.hx(抜粋)
class IntCaller {
    public static inline function callInline(value:Int, callback:Int->Int)
        return callback(value);
}

(準備)コールバック関数

宣言方法を変えながらいくつか用意

Callbacks.hx
class IntCallbacks {
    public static function triple(n:Int)
        return 3 * n;

    public static inline function tripleInline(n:Int)
        return 3 * n;

    public static dynamic function tripleDynamic(n:Int)
        return 3 * n;

    public static final tripleVar = triple; // (n:Int) -> 3 * n とかでもよい
}

結果(どれも同じ)

Callbacks.c
int IntCallbacks_triple(int r0) {
    int r1;
    r1 = 3;
    r1 = r1 * r0;
    return r1;
}

単体実行

普通の関数を渡す

Case00.hx
class Case00 {
    public static function main() {
        IntCaller.call(100, IntCallbacks.triple);
    }
}

結果

Case00.c
#define HLC_BOOT
#include <hlc.h>
#include <_std/Case00.h>
int IntCallbacks_triple(int);
extern hl_type t$fun_75691bc;
int IntCaller_call(int,vclosure*);

void Case00_main() {
    vclosure *r1;
    int r0;
    static vclosure cl$0 = { &t$fun_75691bc, IntCallbacks_triple, 0 };
    r0 = 100;
    r1 = &cl$0;
    r0 = IntCaller_call(r0,r1);
    return;
}

C言語よく分かってないんですが、関数をクロージャ型にするためにアロケートしてたりする??

inline関数を渡す

Case01.hx
class Case01 {
    public static function main() {
        IntCaller.call(100, IntCallbacks.tripleInline);
    }
}

inlineで宣言しても、こういう使い方をした場合は展開されない。よって結果は Case00 と同じ

dynamic関数を渡す

Case02.hx
class Case02 {
    public static function main() {
        IntCaller.call(100, IntCallbacks.tripleDynamic);
    }
}
Case02.c
#define HLC_BOOT
#include <hlc.h>
#include <_std/Case02.h>
#include <_std/IntCallbacks.h>
extern $IntCallbacks g$_IntCallbacks;
int IntCaller_call(int,vclosure*);

void Case02_main() {
    $IntCallbacks r2;
    vclosure *r1;
    int r0;
    r0 = 100;
    r2 = ($IntCallbacks)g$_IntCallbacks;
    r1 = r2->tripleDynamic;
    r0 = IntCaller_call(r0,r1);
    return;
}

Case00(dynamicじゃない普通の関数)と違い、単純に引っ張ってきている(たぶん)

変数に入れた関数を渡す

Case03.hx
class Case03 {
    public static function main() {
        IntCaller.call(100, IntCallbacks.tripleVar);
    }
}

結果: Case02(dynamic関数)と同じ

ループ実行

とりあえずdynamic関数を渡したけど、どのパターンでも特記事項はなかったと思う(たぶん)

Case10.hx
class Case10 {
    public static function main() {
        IntCaller.repeatCall(100, IntCallbacks.tripleDynamic, 2);
    }
}


結果
Case10.c
#define HLC_BOOT
#include <hlc.h>
#include <_std/Case10.h>
#include <_std/IntCallbacks.h>
extern $IntCallbacks g$_IntCallbacks;
int IntCaller_repeatCall(int,vclosure*,int);

void Case10_main() {
    $IntCallbacks r2;
    vclosure *r1;
    int r0, r3;
    r0 = 100;
    r2 = ($IntCallbacks)g$_IntCallbacks;
    r1 = r2->tripleDynamic;
    r3 = 2;
    r0 = IntCaller_repeatCall(r0,r1,r3);
    return;
}

配列ループ実行

普通の関数を渡す

Case20.hx
class Case20 {
    static final array = [100, 200];

    public static function main() {
        IntCaller.forEach(array, IntCallbacks.triple);
    }
}

結果

Case20.c
#define HLC_BOOT
#include <hlc.h>
#include <_std/Case20.h>
int IntCallbacks_triple(int);
extern hl_type t$fun_75691bc;
extern $Case20 g$_Case20;
void IntCaller_forEach(hl__types__ArrayBytes_Int,vclosure*);

void Case20_main() {
    $Case20 r2;
    hl__types__ArrayBytes_Int r1;
    vclosure *r3;
    static vclosure cl$0 = { &t$fun_75691bc, IntCallbacks_triple, 0 };
    r2 = ($Case20)g$_Case20;
    r1 = r2->array;
    r3 = &cl$0;
    IntCaller_forEach(r1,r3);
    return;
}


訂正前
Case20.c
#define HLC_BOOT
#include <hlc.h>
#include <_std/Case20.h>
int IntCallbacks_triple(int);
extern hl_type t$fun_75691bc;
extern $Case20 g$_Case20;
void wrapt$fun_baab521(vclosure*,int);
extern hl_type t$fun_baab521;
void IntCaller_forEach(hl__types__ArrayBytes_Int,vclosure*);

void Case20_main() {
    $Case20 r2;
    hl__types__ArrayBytes_Int r1;
    vclosure *r3, *r4;
    static vclosure cl$0 = { &t$fun_75691bc, IntCallbacks_triple, 0 };
    r2 = ($Case20)g$_Case20;
    r1 = r2->array;
    r3 = &cl$0;
    if( r3 ) goto label$f08e34b_1_6;
    r4 = NULL;
    goto label$f08e34b_1_7;
    label$f08e34b_1_6:
    r4 = hl_alloc_closure_ptr(&t$fun_baab521,wrapt$fun_baab521,r3);
    label$f08e34b_1_7:
    IntCaller_forEach(r1,r4);
    return;
}
  • 渡そうとしている関数(というかvclosure)について、渡す前の段階でnullチェックしている
  • hl_alloc_closure_ptr() というのがある。alloc とあるだけで身構えてしまう。
  • call() のときはこうならなくて forEach() でこうなる理由はよくわからない

→原因判明。

forEach() の定義を

public static function forEach(array:Array<Int>, callback:Int->Void)

としてしまっていて(callback の型が Int->Int ではなく Int-Void になっていた)、
実際に渡す関数と一致していなかった(それでも怒られないのか……?)

(訂正前 ここまで)

dynamic関数を渡す

Case21.hx
class Case21 {
    static final array = [100, 200];

    public static function main() {
        IntCaller.forEach(array, IntCallbacks.tripleDynamic);
    }
}

結果

Case21.c
#define HLC_BOOT
#include <hlc.h>
#include <_std/Case21.h>
#include <_std/IntCallbacks.h>
extern $Case21 g$_Case21;
extern $IntCallbacks g$_IntCallbacks;
void IntCaller_forEach(hl__types__ArrayBytes_Int,vclosure*);

void Case21_main() {
    hl__types__ArrayBytes_Int r1;
    $IntCallbacks r4;
    vclosure *r3;
    $Case21 r2;
    r2 = ($Case21)g$_Case21;
    r1 = r2->array;
    r4 = ($IntCallbacks)g$_IntCallbacks;
    r3 = r4->tripleDynamic;
    IntCaller_forEach(r1,r3);
    return;
}
  • dynamicじゃないときとの違いは call() のときと同様で、
    static vclosure cl$0 = ... のくだりがなくなっている
  • 変数に入れた関数の場合でも、dynamic関数のときと結果に変わりはなかった

call() をinline展開

普通の関数を渡す

Case30.hx
class Case30 {
    public static function main() {
        IntCaller.callInline(100, IntCallbacks.triple);
    }
}

結果。展開するとあっさりである

Case30.c
#define HLC_BOOT
#include <hlc.h>
#include <_std/Case30.h>
int IntCallbacks_triple(int);

void Case30_main() {
    int r0;
    r0 = 100;
    r0 = IntCallbacks_triple(r0);
    return;
}

dynamic関数を渡す

Case31.hx
class Case31 {
    public static function main() {
        IntCaller.callInline(100, IntCallbacks.tripleDynamic);
    }
}

結果

Case31.c
#define HLC_BOOT
#include <hlc.h>
#include <_std/Case31.h>
#include <_std/IntCallbacks.h>
extern $IntCallbacks g$_IntCallbacks;

void Case31_main() {
    $IntCallbacks r2;
    vclosure *r1;
    int r0;
    r2 = ($IntCallbacks)g$_IntCallbacks;
    r1 = r2->tripleDynamic;
    if( r1 == NULL ) hl_null_access();
    r0 = 100;
    r0 = r1->hasValue ? ((int (*)(vdynamic*,int))r1->fun)((vdynamic*)r1->value,r0) : ((int (*)(int))r1->fun)(r0);
    return;
}

今度は、dynamicのほうが結果が長くなった。
Case30よりもコールバック関数へのアクセスが間接的なためか、チェック処理が入ってしまっている。

変数に入れた関数の場合もdynamic関数と同様。

オブジェクトを対象とする処理

(準備)処理対象オブジェクトのクラス 含:各種関数

面倒だったので、コールバック関数もそれを実行する関数も一つのクラスで

IntWrapper.hx
class IntWrapper {
    public static function triple(object:IntWrapper)
        object.value *= 3;

    public static dynamic function tripleDynamic(object:IntWrapper)
        object.value *= 3;

    public static final tripleVar = triple;

    public static function call(value:IntWrapper, callback:IntWrapper->Void)
        callback(value);

    public static function forEach(array:Array<IntWrapper>, callback:IntWrapper->Void)
        for (i in 0...array.length)
            callback(array[i]);

    public var value:Int;

    public function new(value:Int)
        this.value = value;
}

結果

IntWrapper.c
#define HLC_BOOT
#include <hlc.h>
#include <_std/IntWrapper.h>

void IntWrapper_triple(IntWrapper r0) {
    int r1, r2;
    if( r0 == NULL ) hl_null_access();
    r1 = r0->value;
    r2 = 3;
    r1 = r1 * r2;
    r0->value = r1;
    return;
}

void IntWrapper_tripleDynamic(IntWrapper r0) {
    // IntWrapper_triple と同一
}

void IntWrapper_call(IntWrapper r0,vclosure* r1) {
    if( r1 == NULL ) hl_null_access();
    r1->hasValue ? ((void (*)(vdynamic*,IntWrapper))r1->fun)((vdynamic*)r1->value,r0) : ((void (*)(IntWrapper))r1->fun)(r0);
    return;
}

void IntWrapper_forEach(hl__types__ArrayObj r0,vclosure* r1) {
    IntWrapper r7;
    vdynamic *r8;
    varray *r9;
    int r2, r4, r5, r6;
    r2 = 0;
    if( r0 == NULL ) hl_null_access();
    r4 = r0->length;
    label$272c0c7_4_3:
    if( r2 >= r4 ) goto label$272c0c7_4_18;
    r5 = r2;
    ++r2;
    if( r1 == NULL ) hl_null_access();
    if( r0 == NULL ) hl_null_access();
    r6 = r0->length;
    if( ((unsigned)r5) < ((unsigned)r6) ) goto label$272c0c7_4_13;
    r7 = NULL;
    goto label$272c0c7_4_16;
    label$272c0c7_4_13:
    r9 = r0->array;
    r8 = ((vdynamic**)(r9 + 1))[r5];
    r7 = (IntWrapper)r8;
    label$272c0c7_4_16:
    r1->hasValue ? ((void (*)(vdynamic*,IntWrapper))r1->fun)((vdynamic*)r1->value,r7) : ((void (*)(IntWrapper))r1->fun)(r7);
    goto label$272c0c7_4_3;
    label$272c0c7_4_18:
    return;
}

void IntWrapper_new(IntWrapper r0,int r1) {
    r0->value = r1;
    return;
}

単体実行

普通の関数を渡す

Case40.hx
class Case40 {
    static final object = new IntWrapper(100);

    public static function main() {
        IntWrapper.call(object, IntWrapper.triple);
    }
}


結果
Case40.c
#define HLC_BOOT
#include <hlc.h>
#include <_std/Case40.h>
void IntWrapper_triple(IntWrapper);
extern hl_type t$fun_5a742cc;
extern $Case40 g$_Case40;
void IntWrapper_call(IntWrapper,vclosure*);

void Case40_main() {
    $Case40 r2;
    IntWrapper r1;
    vclosure *r3;
    static vclosure cl$0 = { &t$fun_5a742cc, IntWrapper_triple, 0 };
    r2 = ($Case40)g$_Case40;
    r1 = r2->object;
    r3 = &cl$0;
    IntWrapper_call(r1,r3);
    return;
}

特記事項なし。プリミティブ値のときと似てる

dynamic関数を渡す

Case41.hx
class Case41 {
    static final object = new IntWrapper(100);

    public static function main() {
        IntWrapper.call(object, IntWrapper.tripleDynamic);
    }
}


結果
Case41.c
#define HLC_BOOT
#include <hlc.h>
#include <_std/Case41.h>
#include <_std/IntWrapper.h>
extern $Case41 g$_Case41;
extern $IntWrapper g$_IntWrapper;
void IntWrapper_call(IntWrapper,vclosure*);

void Case41_main() {
    $Case41 r2;
    IntWrapper r1;
    vclosure *r3;
    $IntWrapper r4;
    r2 = ($Case41)g$_Case41;
    r1 = r2->object;
    r4 = ($IntWrapper)g$_IntWrapper;
    r3 = r4->tripleDynamic;
    IntWrapper_call(r1,r3);
    return;
}

これも特記事項なし

変数に入れた関数を渡す

  • dynamic関数のときと同じだった
  • なんか違う! なんでや! と思ったのですが、間違えて
    tripleVar = (object:IntWrapper) -> triple;
    としていただけだった(なぜかコンパイラに怒られない)

配列ループ実行

単体実行のときとまったく同じ

まとめ?

渡す関数が以下のどちらで宣言されているかで挙動が違う。

  • 普通の関数
  • dynamic関数 または 変数

dynamic関数と変数とで、なんか違う結果になるケースがあったはずなんだけど再現できない。思い違いか。

プリミティブ値の配列ループ実行の結果だけ特殊なのがよくわからない(なにかミスってそう)
ミスってた。
関数の戻り値の型について、宣言時と使用時で一致しないと無駄な処理が増える上に
コンパイラが怒ったりもしないので気付けない可能性がある(思い違いでは? 要確認)

inline展開のされ方についても要考慮。
展開されないのならdynamic関数または変数が安全牌に見える(ほんとか?)

全体的に、まだよくわからない

FAL
個人でゲームを作ったり、プログラミングでお絵かきしたりしています。 @falworks_ja
http://www.fal-works.com/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした