Ориентация главы
После типов (113) и объектов (114) на сцену выходит динамический вызов: скрипту нужно звать C-функции, C — возвращаться в функции скрипта через callback, связующему слою — передавать аргументы туда-обратно — runtime_call даёт единую модель вызова. Триада: сигнатура (xrtfunctionsig — заимствованные объявления имён параметров/типов/типов возврата; именованные параметры участвуют в идентичности сигнатуры); кадр вызова (xrtcallframe — заимствованная сборка Self/позиционных аргументов/именованных аргументов/контекста; FrameValidate проверяет полностью: дублирующиеся ключевые слова/обязательные параметры/неизвестные ключевые слова/флаги varargs); callable (xrtCallableCreate(签名, 入口, 环境, 环境析构) (сигнатура, вход, среда, деструктор среды) — неизменяем после создания, подсчёт ссылок атомарен; среда удерживается callable). Результат (xrtcallresult — первые четыре инлайн при нулевом выделении, семантика ссылок Set/Push, перенос Take). Шестшаговая атомарная последовательность Invoke (копия кадра → валидация → частный временный результат → очистка при отказе → проверка количества → однократная замена) гарантирует: неудавшийся вызов не портит исходный результат вызывающего. Ключевая граница: даются только типо-явные динамические входы — никакого универсального вызова произвольных C ABI (без зависимости от libffi).
116.1引入
Наивный подход к «динамическим вызовам» — void* fn(void** args) — вся информация о типах потеряна, каждая связь пишет приведения вручную, ошибки взрываются в глубине исполнения. Второй ложный путь — универсальный вызов в духе libffi — бездна платформенных ABI (cdecl/stdcall/fastcall, конвенции возврата структур… контрактная документация говорит прямо: «C не может безопасно универсально вызывать произвольные функции без точного прототипа или libffi»). Третий путь xruntime: типо-явные динамические входы xrtcallproc — компилятор, генератор связок или FFI-модуль порождает по одному адаптеру на каждую исходную функцию (у адаптера точный прототип, вызов исходной функции с нулём UB), и динамический слой видит только адаптеры. Сигнатуры циркулируют в динамическом слое: имена параметров/типы/типы возврата (подпись каждого) — всё объявлено — вход получает «проверенный кадр + действительную сигнатуру», приведения выполняются статически внутри адаптера.
Дизайн именованных параметров заслуживает внимания: KeywordNames/KeywordValues — заимствованные массивы имён и значений — не словарь! «Вызывающему не нужно строить словарь, чтобы передать kwargs» — ноль выделений на горячем пути в очередной раз (сравните с обычной практикой скриптовых языков: сперва Map, потом передача).
116.2概念
Сигнатура и кадр вызова
Три входа чтения параметров (представление аргументов): FrameArgument/FrameKeyword (сырые виды переданных аргументов), FrameParameter (выбирает позиционный/именованный по индексу формального параметра действующей сигнатуры — предпочтительный обычный вход; непереданный опциональный параметр возвращает NULL без ошибки).
Результат: инлайн и семантика ссылок
xrtcallresult владеет ссылками xvalue внутри: первые четыре результата инлайн (ноль выделений массива результатов); с пятого — лёгкий массив указателей (динамический Value-контейнер не строится). Семейство операций: Set/Push (наращивает ссылку), SetTake/PushTake (переносит и очищает исходный слот), Clear (освобождает значения, сохраняя ёмкость — переиспользование горячего пути), Unit (освобождает всё), Move (переносит целиком). Разрешена только замена существующего индекса или добавление в конец — разреженная запись запрещена (урок старой версии, строившей множество null для заполнения дыр — количество возвратов всегда явно).
callable и шесть шагов Invoke
Вход можно звать конкурентно и реентерабельно — конкурентная безопасность среды — ответственность реализации входа (контракт чертит границу). Тип callable (xrtTypeCallable()): стабильный C-ABI-слот, держащий одну сильную ссылку — Copy/Clone наращивает и заменяет, Move переносит и очищает, Drop освобождает — callable циркулируют как значения в контейнерах/полях (тот же паттерн, что у значений-объектов главы 115).
Граница сырого ABI
Старый callable хранил смесь указателей cdecl/stdcall/fastcall и динамический вход — этот модуль даёт только один типо-явный динамический вход. Генераторы связок эмитят адаптеры xrtcallproc, вызывающие исходную функцию: эта граница избегает неопределённого поведения и не накладывает платформенную цену ABI на базовый слой вызова (выигрыш обрезки и переносимости одновременно).
Положение слоя вызова
116.3示例
Первая полная программа: создание и вызов callable
Программа ниже из examples/runtime/call — полный цикл сигнатуры, кадра и вызова:
#include <stdio.h> #include <xruntime.h> /* 把两个整数参数相加并移交唯一的返回值引用。 */ static bool add( ptr pEnvironment, const xrtcallframe* pFrame, xrtcallresult* pResult ) { int64 iLeft; int64 iRight; xvalue* pValue; (void)pEnvironment; if ( !xrtValueGetInt(xrtCallFrameParameter(pFrame, 0u), &iLeft) || !xrtValueGetInt(xrtCallFrameParameter(pFrame, 1u), &iRight) ) { return false; } pValue = xrtValueInt(iLeft + iRight); if ( pValue == NULL ) { return false; } if ( !xrtCallResultPushTake(pResult, &pValue) ) { xrtValueRelease(pValue); return false; } return true; } int main(void) { xrtparamdesc Params[2] = { { XRT_STR_INIT("left"), xrtTypeInt64(), XRT_PARAM_DEFAULT, 0u }, { XRT_STR_INIT("right"), xrtTypeInt64(), XRT_PARAM_DEFAULT, 0u } }; const xrttype* ReturnType = xrtTypeInt64(); xrtfunctionsig Signature = { .Name = XRT_STR_INIT("add"), .ParamCount = 2u, .Params = Params, .ReturnCount = 1u, .ReturnTypes = &ReturnType }; xvalue* Arguments[2] = { xrtValueInt(7), xrtValueInt(5) }; xrtcallframe Frame = { .ArgumentCount = 2u, .Arguments = Arguments }; xrtcallresult Result = XRT_CALL_RESULT_INIT; xrtcallable* pCallable = xrtCallableCreate( &Signature, add, NULL, NULL); int64 iSum; int iResult = 0; if ( (pCallable == NULL) || !xrtCallableInvoke(pCallable, &Frame, &Result) || !xrtValueGetInt(xrtCallResultGet(&Result, 0u), &iSum) ) { iResult = 1; } else { printf("type=%.*s\n", (int)xrtTypeCallable()->Name.Size, xrtTypeCallable()->Name.Data); printf("7 + 5 = %lld\n", (long long)iSum); } xrtCallResultUnit(&Result); xrtCallableUnref(pCallable); xrtValueRelease(Arguments[1]); xrtValueRelease(Arguments[0]); return iResult; }
Что только что произошло. (1) Сторона создания: сигнатура (два параметра int64, возврат int64 — заимствованное объявление) + вход сложения + среда компонуют callable — xrtTypeCallable()->Name печатает callable: callable сам — типизированное значение (категория CALLABLE из спектра типов главы 114). (2) Сторона вызова: кадр собирает два позиционных аргумента → xrtCallableInvoke(pCallable, &Frame, &Result) → xrtCallResultGet(&Result, 0) берёт первый результат → xrtValueGetInt разыменовывает — четыре шага от динамического результата обратно к статическому int64. (3) За выводом 7 + 5 = 12 стоит полная шестшаговая последовательность: кадр проверен, временный результат атомарно заменён — при отказе на любом шаге ваш Result остаётся нетронутым (следующий вызов может безопасно его переиспользовать). Unit в финале + Unref callable — дисциплина ссылок та же, что у объектов главы 115.
Вторая полная программа: упаковка callable на стороне значений
Вторая программа из examples/runtime/value_callable — callable входит в мир Value:
#include <stdio.h> #include <xruntime.h> /* 返回一个整数。 */ static bool answer( ptr pEnvironment, const xrtcallframe* pFrame, xrtcallresult* pResult ) { xvalue* pValue = xrtValueInt((int64)(intptr_t)pEnvironment); (void)pFrame; if ( pValue == NULL ) { return false; } if ( !xrtCallResultPushTake(pResult, &pValue) ) { xrtValueRelease(pValue); return false; } return true; } int main(void) { xrtcallable* pCallable = xrtCallableCreate( NULL, answer, (ptr)(intptr_t)42, NULL); xvalue* pFunction = xrtValueCallableTake(&pCallable); xrtcallresult Result = XRT_CALL_RESULT_INIT; int64 iValue; int iResult = 0; if ( (pFunction == NULL) || !xrtValueInvoke(pFunction, NULL, &Result) || !xrtValueGetInt(xrtCallResultGet(&Result, 0u), &iValue) ) { iResult = 1; } else { printf("answer=%lld\n", (long long)iValue); } xrtCallResultUnit(&Result); xrtValueRelease(pFunction); return iResult; }
Что только что произошло. (1) runtime_value_callable — независимый слой обрезки: сильная ссылка callable упаковывается в xvalue (динамические значения главы 31, расширение времени исполнения) — здесь приземляется скриптовое «функции — значения первого класса»: callable внутри Value можно хранить в dict, передавать в функции и возвращать из них. (2) Базовый runtime_call не принуждает включать упаковку Value — сценарии без первоклассности не платят за упаковку (философия обрезки главы 114, послойно). (3) Семья сопроводительных образцов: value_future (упаковка Future), value_object (упаковка объекта), value_type (упаковка значения-типа) — полный мост между объектами времени исполнения и динамическими значениями.
116.4契约
- Заимствование сигнатур: массивы сигнатуры/типов/параметров/возвратов держит объявитель, жизнь покрывает callable; именованные параметры участвуют в идентичности сигнатуры и уникальны; пустые имена — только позиционно.
- Заимствование кадров: Self/массивы аргументов/именованных/контекст — всё заимствовано; действительно на время вызова.
- Валидация полностью: целостность/пустые значения/дубли ключевых слов/обязательные/двойная передача/неизвестные ключевые слова/комбинации флагов — до входа доходит только проверенный кадр.
- Чтение параметров: FrameParameter выбирает позиционный/именованный по индексу сигнатуры (предпочтителен); непереданный опциональный = NULL без ошибки; несуществующее ключевое = NULL без ошибки.
- Семантика результата: первые четыре инлайн; Set/Push наращивают ссылки, семейство Take переносит и очищает источник; Clear сохраняет ёмкость; только замена или добавление — без разреженной записи.
- Callable неизменяем: сигнатура/вход/среда не меняются после создания; счёт ссылок атомарен; среда деструктурируется ровно один раз.
- Атомарность Invoke: шестшаговая последовательность; при отказе частичные результаты освобождаются + цепочка причин оборачивается, результат вызывающего цел; при успехе однократная замена.
- Дисциплина входа: конкурентно и реентерабельно; конкурентная безопасность среды — на совести реализации входа.
- Граница ABI: ровно один типо-явный вход; исходные функции подключаются адаптерами — без универсального вызова и UB.
- Независимая упаковка: Value callable — независимая обрезка; базовый слой без цены упаковки.
Типичные ошибки
Симптом: callable при вызове читает мусорную сигнатуру — массивы параметров/возвратов времени создания умерли со стековым кадром.
Причина: сигнатура заимствована — её жизненный цикл обязан покрывать весь жизненный цикл callable (callable её не копирует). Кладите сигнатуры в статическое хранилище или в кучу, живущую столько же, сколько callable.
xrtcallable* make(void) { xrtfunctionsig Sig = { ...栈上... }; return xrtCallableCreate(&Sig, entry, NULL, NULL); /* висячая по возврату */ }
static const xrttype* const kArgs[] = { ... }; /* статика */ static const xrtfunctionsig kSig = { ... }; xrtcallable* C = xrtCallableCreate(&kSig, entry, NULL, NULL);
Симптом: путь отказа читает Result — а там старое значение предыдущего успешного вызова (атомарность его защитила, но это не результат текущего вызова).
Причина: шестшаговая последовательность гарантирует «отказ не портит исходный результат» — исходный результат может быть пуст (первый вызов) или старым значением; после отказа семантика Result — «не изменился», надо идти в ветку ошибки, а не читать результат.
if ( !xrtCallableInvoke(C, &Frame, &Result) ) { use(xrtCallResultGet(&Result, 0)); /* старое/пустое - не текущее */ }
if ( !xrtCallableInvoke(C, &Frame, &Result) ) { report(xrtErrorMessage(xrtGetError())); /* ветка ошибки: смотреть только ошибку */ } else { use(xrtCallResultGet(&Result, 0)); /* результат текущего вызова только при успехе */ }
Симптом: отвергнуто — результаты разрешают только замену существующего индекса или добавление в конец.
Причина: семантическая дыра разреженной записи (что в промежуточных индексах? null?) была ямой старого дизайна — новый контракт держит «количество возвратов всегда явно»; нужен заполнитель — явно Push пустое.
xrtCallResultSet(&Result, 2u, Value); /* вне диапазона, разреженно: отвергнуто */
/* добавлять по порядку - количество и порядок оба явны */ xrtCallResultPush(&Result, V0); xrtCallResultPush(&Result, V1); xrtCallResultPush(&Result, V2);
Практические задания
Один вход: вызов с позиционными аргументами + вызов с именованными (передача массивом имён) — FrameParameter читает обе колеи. Критерии приёмки: обе колеи дают одни и те же параметры; неизвестные ключевые слова отвергаются Validate.
Сигнатура из трёх параметров (один обязательный, два опциональных): вход берёт умолчания для непереданных — NULL-семантика FrameParameter. Критерии приёмки: все три формы вызова (все даны/один дан/только обязательный) ведут себя верно; двойная передача (позиционный + именованный с одним именем) отвергается.
Dict (глава 32) хранит реестр name→callable; реализуйте мини-интерпретатор call(name, 帧字符串) (строка кадра): поиск в таблице → кадр → Invoke → печать результата. Добавьте одну исходную C-функцию (например, max(int64,int64)) через адаптер. Критерии приёмки: три функции зарегистрированы и вызываемы; неизвестное имя сообщает чисто; конкурентные вызовы одного callable без ошибок (вход без состояния).
Итоги
| Тема | Кратко |
|---|---|
| Триада | сигнатура (заимствованное объявление) / кадр (заимствованная сборка) / callable (неизменяем + атомарные ссылки) |
| Именованные параметры | заимствованные массивы имён и значений — не словарь; имена в идентичности сигнатуры, уникальны; пустые имена только позиционно |
| Валидация | Validate полностью: дубли ключевых слов/обязательные/неизвестные/комбинации varargs |
| Чтение параметров | FrameParameter по индексу сигнатуры (предпочтителен); непереданный опциональный = NULL без ошибки |
| Результат | первые четыре инлайн; Set/Push ссылки, Take перенос, Clear хранит ёмкость; только замена/добавление |
| Шесть шагов Invoke | копия кадра → валидация → временный результат → очистка при отказе → проверка количества → однократная замена |
| Атомарность | отказ не портит исходный результат вызывающего; ошибка обёрнута в цепочку причин xrt.call |
| Вход | конкурентно и реентерабельно; конкурентность среды — реализация |
| Граница ABI | ровно один типо-явный вход; исходные функции через адаптеры — без UB, без платформенной цены |
| Упаковка | Value callable — независимая обрезка; функции первого класса опциональны |
📖 Справочники API в этой главе:xruntime-runtime_call (21 записей)xruntime-runtime_type (60 записей)
Полные сигнатуры, ограничения параметров, возвращаемые значения и ошибки