Ориентация главы

После типов (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概念

Сигнатура и кадр вызова

1Сигнатураxrtfunctionsig - массивы параметров (имя/тип/опциональность) и типов возврата (заимствованы у объявителя, жизнь покрывает callable)
2Кадрxrtcallframe - Self (опциональный получатель) / Arguments (позиционные; за формальными - varargs)
3ВалидацияFrameValidate - целостность массивов/пустые значения/дубли ключевых слов/обязательные/двойная передача/неизвестные ключевые слова/комбинации флагов

Три входа чтения параметров (представление аргументов): FrameArgument/FrameKeyword (сырые виды переданных аргументов), FrameParameter (выбирает позиционный/именованный по индексу формального параметра действующей сигнатуры — предпочтительный обычный вход; непереданный опциональный параметр возвращает NULL без ошибки).

Результат: инлайн и семантика ссылок

xrtcallresult владеет ссылками xvalue внутри: первые четыре результата инлайн (ноль выделений массива результатов); с пятого — лёгкий массив указателей (динамический Value-контейнер не строится). Семейство операций: Set/Push (наращивает ссылку), SetTake/PushTake (переносит и очищает исходный слот), Clear (освобождает значения, сохраняя ёмкость — переиспользование горячего пути), Unit (освобождает всё), Move (переносит целиком). Разрешена только замена существующего индекса или добавление в конец — разреженная запись запрещена (урок старой версии, строившей множество null для заполнения дыр — количество возвратов всегда явно).

callable и шесть шагов Invoke

не-созданCreate(сигнатура, вход, среда, деструктор) - неизменяем; среду держит callable →готов
в-вызовеосвободить все частичные результаты + обернуть ошибку входа в цепочку причин xrt.call (исходный результат вызывающего цел) →отказ
в-вызовепри наличии сигнатуры сверить точное число возвратов -> однократная замена результата вызывающего →успех

Вход можно звать конкурентно и реентерабельно — конкурентная безопасность среды — ответственность реализации входа (контракт чертит границу). Тип callable (xrtTypeCallable()): стабильный C-ABI-слот, держащий одну сильную ссылку — Copy/Clone наращивает и заменяет, Move переносит и очищает, Drop освобождает — callable циркулируют как значения в контейнерах/полях (тот же паттерн, что у значений-объектов главы 115).

Граница сырого ABI

Старый callable хранил смесь указателей cdecl/stdcall/fastcall и динамический вход — этот модуль даёт только один типо-явный динамический вход. Генераторы связок эмитят адаптеры xrtcallproc, вызывающие исходную функцию: эта граница избегает неопределённого поведения и не накладывает платформенную цену ABI на базовый слой вызова (выигрыш обрезки и переносимости одновременно).

Положение слоя вызова

1Слой сигнатурxrtfunctionsig - заимствованные объявления параметров/возвратов (имена участвуют в идентичности)
2Слой кадраxrtcallframe - заимствованная сборка Self/позиционных/именованных/контекста + Validate полностью
3Слой callableнеизменяемый объект + атомарные ссылки; среду держит он
4Слой результатаxrtcallresult - первые четыре инлайн, семантика ссылок/переноса, только замена или добавление
5Слой границыединственный типо-явный динамический вход - исходные функции подключаются адаптерами

116.3示例

Первая полная программа: создание и вызов callable

Программа ниже из examples/runtime/call — полный цикл сигнатуры, кадра и вызова:

extlibs/xruntime/examples/runtime/call/main.c源码 ↗
#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;
}
$ gcc -O1 -DXRT_MODULE_ALL -DXRUNTIME_MODULE_ALL -I single -I single/extlibs -include xrt.h -include xruntime.h impl.c extlibs/xruntime/examples/runtime/call/main.c -lws2_32 -lbcrypt -ladvapi32 -liphlpapitype=callable7 + 5 = 12

Что только что произошло. (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:

extlibs/xruntime/examples/runtime/value_callable/main.c源码 ↗
#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;
}
$ gcc -O1 -DXRT_MODULE_ALL -DXRUNTIME_MODULE_ALL -I single -I single/extlibs -include xrt.h -include xruntime.h impl.c extlibs/xruntime/examples/runtime/value_callable/main.c -lws2_32 -lbcrypt -ladvapi32 -liphlpapi(输出 callable 装入 Value 后的调用自检结果)

Что только что произошло. (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 — независимая обрезка; базовый слой без цены упаковки.

Типичные ошибки

Ловушка 1: массив сигнатуры — стековая переменная:Ловушка 1: массив сигнатуры — стековая переменная

Симптом: 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);
Ловушка 2: использование результата сразу после неудавшегося вызова:Ловушка 2: использование результата сразу после неудавшегося вызова

Симптом: путь отказа читает Result — а там старое значение предыдущего успешного вызова (атомарность его защитила, но это не результат текущего вызова).

Причина: шестшаговая последовательность гарантирует «отказ не портит исходный результат» — исходный результат может быть пуст (первый вызов) или старым значением; после отказа семантика Result — «не изменился», надо идти в ветку ошибки, а не читать результат.

错误写法
if ( !xrtCallableInvoke(C, &Frame, &Result) ) {
	use(xrtCallResultGet(&Result, 0));   /* старое/пустое - не текущее */
}
正确写法
if ( !xrtCallableInvoke(C, &Frame, &Result) ) {
	report(xrtErrorMessage(xrtGetError()));   /* ветка ошибки: смотреть только ошибку */
} else {
	use(xrtCallResultGet(&Result, 0));         /* результат текущего вызова только при успехе */
}
Ловушка 3: разреженная запись результатов (Set индекс 2, когда есть только 0):Ловушка 3: разреженная запись результатов (Set индекс 2, когда есть только 0)

Симптом: отвергнуто — результаты разрешают только замену существующего индекса или добавление в конец.

Причина: семантическая дыра разреженной записи (что в промежуточных индексах? null?) была ямой старого дизайна — новый контракт держит «количество возвратов всегда явно»; нужен заполнитель — явно Push пустое.

错误写法
xrtCallResultSet(&Result, 2u, Value);   /* вне диапазона, разреженно: отвергнуто */
正确写法
/* добавлять по порядку - количество и порядок оба явны */
xrtCallResultPush(&Result, V0);
xrtCallResultPush(&Result, V1);
xrtCallResultPush(&Result, V2);

Практические задания

Базовое: callable сложения на двух колеях аргументов

Один вход: вызов с позиционными аргументами + вызов с именованными (передача массивом имён) — 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 записей)

Полные сигнатуры, ограничения параметров, возвращаемые значения и ошибки