Open3

ALTREP について調べたことをメモる

yutannihilationyutannihilation

R_make_altTYPE_class()の3つめの引数には DllInfo を渡さないといけないのか?

GitHub を検索する感じ、ここには DLL が渡されるらしい。パッケージのロード時にR_registerRoutines()とかするところで一緒に登録するのが正しそう。

https://github.com/search?q=org%3Acran+R_make_altreal_class&type=code

しかし、extendrってそんなことしてたっけ?と思ってextendrの実装を見ると、どうやらそうなってないらしい

            let class_ptr = match ty {
                Rtype::Integers => {
                    R_make_altinteger_class(csname.as_ptr(), csbase.as_ptr(), std::ptr::null_mut())
                }
                Rtype::Doubles => {
                    R_make_altreal_class(csname.as_ptr(), csbase.as_ptr(), std::ptr::null_mut())
                }
                Rtype::Logicals => {
                    R_make_altlogical_class(csname.as_ptr(), csbase.as_ptr(), std::ptr::null_mut())
                }
                Rtype::Raw => {
                    R_make_altraw_class(csname.as_ptr(), csbase.as_ptr(), std::ptr::null_mut())
                }
                Rtype::Complexes => {
                    R_make_altcomplex_class(csname.as_ptr(), csbase.as_ptr(), std::ptr::null_mut())
                }
                Rtype::Strings => {
                    R_make_altstring_class(csname.as_ptr(), csbase.as_ptr(), std::ptr::null_mut())
                }
                #[cfg(use_r_altlist)]
                Rtype::List => {
                    R_make_altlist_class(csname.as_ptr(), csbase.as_ptr(), std::ptr::null_mut())
                }
                _ => panic!("expected Altvec compatible type"),
            };

ただ、R 本体のコードを見ると、どうも base なら NULL でも動くようになってるっぽいので、DllInfo がなくても。ただ、これは NULL であって null pointer ではないので、やっぱり extendr のコードともちょっと違う気がする。

https://github.com/wch/r-source/blob/ab9c4114cd774d359f6d4c3b4a69e7ba9d2864c8/src/main/altclasses.c#L524-L525

R_make_altTYPE_class()は何をやっているのか

R_make_altinteger_class()でRのソースコードを検索しても出てこなくて、実際にはこういうマクロで定義されている(src/main/altrep.c)。

/*  Using macros like this makes it easier to add new methods, but
    makes searching for source harder. Probably a good idea on
    balance though. */
#define DEFINE_CLASS_CONSTRUCTOR(cls, type)			\
    R_altrep_class_t R_make_##cls##_class(const char *cname,	\
					  const char *pname,	\
					  DllInfo *dll)		\
    {								\
	return  make_altrep_class(type, cname, pname, dll);	\
    }

そして、make_altrep_class()は、

static R_altrep_class_t
make_altrep_class(int type, const char *cname, const char *pname, DllInfo *dll)
{
    SEXP class;
    switch(type) {
    case INTSXP:  MAKE_CLASS(class, altinteger); break;
    case REALSXP: MAKE_CLASS(class, altreal);    break;
    case LGLSXP:  MAKE_CLASS(class, altlogical); break;
    case RAWSXP:  MAKE_CLASS(class, altraw);     break;
    case CPLXSXP: MAKE_CLASS(class, altcomplex); break;
    case STRSXP:  MAKE_CLASS(class, altstring);  break;
    case VECSXP:  MAKE_CLASS(class, altlist);    break;
    default: error("unsupported ALTREP class");
    }
    RegisterClass(class, type, cname, pname, dll);
    return R_cast_altrep_class(class);
}

MAKE_CLASS()dllを使っていないのでひとまず置いておいて、RegisterClass() を見てみる。

static void
RegisterClass(SEXP class, int type, const char *cname, const char *pname,
	      DllInfo *dll)
{
    PROTECT(class);
    if (Registry == NULL) {
	Registry = CONS(R_NilValue, R_NilValue);
	R_PreserveObject(Registry);
    }


    SEXP csym = install(cname);
    SEXP psym = install(pname);
    SEXP stype = PROTECT(ScalarInteger(type));
    SEXP iptr = R_MakeExternalPtr(dll, R_NilValue, R_NilValue);
    SEXP entry = LookupClassEntry(csym, psym);
    if (entry == NULL) {
	entry = list4(class, psym, stype, iptr);
	SET_TAG(entry, csym);
	SETCDR(Registry, CONS(entry, CDR(Registry)));
    }
    else {
	SETCAR(entry, class);
	SETCAR(CDR(CDR(entry)), stype);
	SETCAR(CDR(CDR(CDR(entry))), iptr);
    }
    SET_ALTREP_CLASS_SERIALIZED_CLASS(class, csym, psym, stype);
    UNPROTECT(2); /* class, stype */
}

↑でやってることは要は、DllInfoのポインタのアドレスを保存しておいて(SETCAR()で紐づけるためにわざわざ EXTPTRSEXP 化している)、あとで同じものかどうかチェックするのに使いたい、ということらしい。DllInfoの中身自体を使っているわけではない

static void reinit_altrep_class(SEXP sclass);
attribute_hidden void R_reinit_altrep_classes(DllInfo *dll)
{
    for (SEXP chain = CDR(Registry); chain != R_NilValue; chain = CDR(chain)) {
	SEXP entry = CAR(chain);
	SEXP iptr = CAR(CDR(CDR(CDR(entry))));
	if (R_ExternalPtrAddr(iptr) == dll)
	    reinit_altrep_class(CAR(entry));
    }
}

reinit_altrep_class() の中身はメソッドの関数をデフォルトの関数に置き換えている。これが実行されないと何が困るのか?

#define INIT_CLASS(cls, type) do {				\
	*((type##_methods_t *) (CLASS_METHODS_TABLE(cls))) =	\
	    type##_default_methods;				\
    } while (FALSE)

R_reinit_altrep_classes() が呼ばれるのは DeleteDLL() という関数の中で、つまり、たぶん dyn.unload() とかするときに defunct な pointer を参照したままになってしまう、ということっぽい。大丈夫なのか...?

https://github.com/wch/r-source/blob/ab9c4114cd774d359f6d4c3b4a69e7ba9d2864c8/src/main/Rdynload.c#L712

yutannihilationyutannihilation

data2の正しい使い方は謎。 Arrow の場合、materialize したデータは data2 に直接置かずに CAR() に置いている。これは factor の場合に leveldata2 に入れときたい、みたいなメタデータ的な扱いだかららしい。でも別に、

  • 元のデータに持たせておく
  • 別の場所(OnceCell でグローバルな変数をつくっとくとか)に置いておく

というのでもいい気もしていて、 data2 が最適解なのかはよくわからない。