ALTREP について調べたことをメモる
R_make_altTYPE_class()
の3つめの引数には DllInfo
を渡さないといけないのか?
GitHub を検索する感じ、ここには DLL が渡されるらしい。パッケージのロード時にR_registerRoutines()
とかするところで一緒に登録するのが正しそう。
しかし、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 のコードともちょっと違う気がする。
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 を参照したままになってしまう、ということっぽい。大丈夫なのか...?
実際の実装は Arrow のが参考になった。 Materialize()
というメソッドを定義しておいて、とりあえずそれを呼んで ALTREP じゃない、通常の SEXP を生成したらなんとかなる。coerce とか duplicate もそれを渡せばいい
data2
の正しい使い方は謎。 Arrow の場合、materialize したデータは data2
に直接置かずに CAR()
に置いている。これは factor の場合に level
を data2
に入れときたい、みたいなメタデータ的な扱いだかららしい。でも別に、
- 元のデータに持たせておく
- 別の場所(
OnceCell
でグローバルな変数をつくっとくとか)に置いておく
というのでもいい気もしていて、 data2
が最適解なのかはよくわからない。