Сегодня весь день занимался тем, что в приличном обществе называют ненормальным программированием:
По списку имен интерфейсов (задается пользователем), в каждом из которых все методы имеют вид
#[id(some_id)]
fn foo(&mut self, event: E);
сгенерировать реализацию
trait Dispatch {
fn dispatch(&mut self, id: usize, event: E);
}
Все просто? Только это Rust, здесь нет рефлексии, так что каждый пользовательский интерфейс придется промазать смазать процмакросней, генерящей что-то типа
impl dyn Iface {
fn dispatch<S: Iface ?Sized>(&mut S, id, event)
{ и тут match по id }
}
А дальше очевидно, делаем
impl Dispatch for UserType
where Self : все интерфейсы {
fn dispatch(&mut self, id, event) {
<dyn Iface>::dispatch(self, id, event); // повторить для всех интерфейсов
}
}
Ну и вроде все, дело в шляпе...
Только вот если интерфейсов много, методов в них много и еще id разрозненные и не образуют непрерывных диапазонов, компиль сотни последовательных сравнений id с джампами, что плакать хочется.
Что же делать?
Надо собрать один большой vtable, чтоб делать
fn dispatch(&mut self, id, event) {
vtable[id](self, event)
}
Что может пойти не так?
ВСЁ
Собрать его надо в compile time, чтоб он единожды посчитанной константой
И константа эта должна быть параметризована пользовательским типом. Который в свою очередь тоже может иметь generic параметры...
Rust позволит сделать что-то такое
impl dyn Iface {
const fn vtable<S:Iface>() -> [(Id, fn(&mut S, e); METHODS_COUNT] { собрать и вернуть массив }
}
Константа, все как надо!
Но в этот момент открывается бездна боли текущего состояния const generics в Rust:
const VTABLE = make_vtable_for!(UserStruct, Iface1, Iface2....);
const низя если UserStruct имеет generic параметры
зато можно так
let vtable = const {
make_vtable_for!(...)
};
Ну да ладно. Все проблемы они внутри make_vtable_for....
Потому что этому страшному макросу нужно проделать самую сложную операцию: сконкатенировать массивы...
const fn concat<N, M, T>([T; N], [T;M]) -> [T; {N M}] — такое не поддерживается.
Надо итоговый размер самому подставить — тогда прокатит...
Для этого нужно узнать суммарную длину. И сохранить ее как константу
const N = <dyn Iface1>::vtable::<UserType>().len();
const M = <dyn Iface2>::vtable::<UserType>().len();
const R = N M;
Ой, тут тоже нельзя const ведь UserType модет иметь дженерик параметры...
Что, конкатенация идет лесом?
Э, нет! Ведь мы можем сгенерить специальный тип заглушку, который точно не имеет дженериков, и который мы будем использовать, только лишь чтою узнать размер vtable
const N = <dyn Iface1>::vtable::<Stub>().len();
const M = <dyn Iface2>::vtable::<Stub>().len();
const TOТАL= N M;
concat<TOTAL>(
<dyn Iface1>::vtable::<UserType>(),
<dyn Iface2>::vtable::<UserType>(),
)
Красота.
Дальше остснется рекурсивно декларативный макрос написать.
Но если его написать в лоб, размер генерируемого Rust кода, полного констант, будет расти экспоненциально от числа интерфейсов, вешая нехер IDE.
Поэтому пришлось написать еще два макроса, чтоб рассчитать весь размер vtable заранее, а не собирать его на каждом уровне вызова макроса.
Красиво.
В конечном итоге я соорудил всю эту красоту, проверил генеренный бинарь, чтоб в нем не было лишнего мусора — о чудо, его там нет.
Даже моей могучей константы vtable нету. А диспатч превратился в развертку бинарного поиска