Рабочее определение замыкания с примером JavaScript
Замыкание - это вид объекта, который содержит указатель или ссылку некоторого вида на функцию, которая должна быть выполнена вместе сэкземпляр данных, необходимых для функции.
Пример на JavaScript из https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures -
function makeAdder(x) {
return function(y) { // create the adder function and return it along with
return x + y; // the captured data needed to generate its return value
};
}
, который затем можно использовать как:
var add5 = makeAdder(5); // create an adder function which adds 5 to its argument
console.log(add5(2)); // displays a value of 2 + 5 or 7
Некоторые из препятствий, которые необходимо преодолеть с помощью C
Язык программирования C является статически типизированным языком, в отличие от JavaScript, не имеет сборки мусора и некоторых других функций, которые делают егопростое создание замыканий в JavaScript или других языках с внутренней поддержкой замыканий.
Одним из больших препятствий для замыканий в стандарте C является отсутствие языковой поддержки для типа конструкции в примере JavaScript, в котором замыкание не включаеттолько функция, а также копия данных, которые собираются при создании замыкания, способ сохранения состояния, который затем может бытьиспользуется, когда замыкание выполняется вместе с любыми дополнительными аргументами, предоставляемыми в то время, когда вызывается функция замыкания.
Однако C имеет некоторые базовые строительные блоки, которые могут предоставить инструменты для создания своего рода замыкания.Некоторые из трудностей заключаются в том, что (1) управление памятью является обязанностью программиста, нет сборки мусора, (2) функции и данные разделены, нет классов или механики типов классов, (3) статически типизированы, поэтому не требуется обнаружение типов данных во время выполненияили размерами данных, и (4) плохими языковыми возможностями для захвата данных о состоянии во время создания замыкания.
Одна вещь, которая делает возможной функцию замыкания с помощью C, это указатель void *
и использование unsigned char
как тип памяти общего назначения, который затем преобразуется в другие типы посредством приведения.
Реализация со стандартом C и немного растяжения здесь и там
ПРИМЕЧАНИЕ: Следующий пример зависит от соглашения о передаче аргументов на основе стека, которое используется в большинстве 32-разрядных компиляторов x86.Большинство компиляторов также позволяют указывать соглашение о вызовах, отличное от передачи аргументов на основе стека, например, модификатор __fastcall
в Visual Studio.По умолчанию для 64-битной и 64-битной Visual Studio используется соглашение __fastcall
по умолчанию, чтобы аргументы функции передавались в регистрах, а не в стеке.См. Обзор соглашений о вызовах x64 в Microsoft MSDN, а также Как установить аргументы функций в сборке во время выполнения в 64-битном приложении в Windows? , а также различные ответы и комментарии в Как переменные аргументы реализованы в gcc? .
Одна вещь, которую мы можем сделать, - решить эту проблему, предоставив некоторую возможность закрытия для C, - это упростить проблему.Лучше обеспечить 80% -ное решение, которое полезно для большинства приложений, чем вообще никакого решения.
Одним из таких упрощений является поддержка только тех функций, которые не возвращают значение, другими словами, функции, объявленные как void func_name()
.Мы также собираемся отказаться от проверки типа времени компиляции списка аргументов функции, так как этот подход создает список аргументов функции во время выполнения.Ни одна из этих вещей, от которых мы отказываемся, не тривиальна, поэтому вопрос в том, перевешивает ли ценность этого подхода для замыканий в C то, что мы отказываемся.
Прежде всего, давайте определим нашу область данных замыканий.Область данных замыкания представляет собой область памяти, которую мы будем использовать для хранения информации, необходимой для замыкания.Минимальный объем данных, о котором я могу думать, - это указатель на функцию, которую нужно выполнить, и копия данных, которые будут предоставлены функции в качестве аргументов.
В этом случае мы собираемся предоставить любые захваченные данные о состояниинужен функции в качестве аргумента функции.
Мы также хотим иметь некоторые базовые средства безопасности, чтобы мы могли достаточно безопасно потерпеть неудачу.К сожалению, рельсы безопасности немного слабы, так как некоторые обходные пути, которые мы используем для реализации формы замыканий.
Исходный код
Следующий исходный код былразработано с использованием Visual Studio 2017 Community Edition в исходном файле .c C.
Область данных - это структура, содержащая некоторые данные управления, указатель на функцию и область данных открытого состава.
typedef struct {
size_t nBytes; // current number of bytes of data
size_t nSize; // maximum size of the data area
void(*pf)(); // pointer to the function to invoke
unsigned char args[1]; // beginning of the data area for function arguments
} ClosureStruct;
Далее мы создаем функцию, которая инициализирует область данных закрытия.
ClosureStruct * beginClosure(void(*pf)(), int nSize, void *pArea)
{
ClosureStruct *p = pArea;
if (p) {
p->nBytes = 0; // number of bytes of the data area in use
p->nSize = nSize - sizeof(ClosureStruct); // max size of the data area
p->pf = pf; // pointer to the function to invoke
}
return p;
}
Эта функция предназначена для приема указателя на область данных, что дает гибкость в отношении того, как пользователь функциихочет управлять памятью.Они могут использовать некоторую память в стеке или статическую память, или они могут использовать память кучи через функцию malloc()
.
unsigned char closure_area[512];
ClosureStruct *p = beginClosure (xFunc, 512, closure_area);
или
ClosureStruct *p = beginClosure (xFunc, 512, malloc(512));
// do things with the closure
free (p); // free the malloced memory.
Далее мы предоставляем функцию, котораяпозволяет нам добавлять данные и аргументы к нашему закрытию.Цель этой функции состоит в том, чтобы построить данные закрытия так, чтобы при вызове функции закрытия функции закрытия предоставлялись все данные, необходимые для ее работы.
ClosureStruct * pushDataClosure(ClosureStruct *p, size_t size, ...)
{
if (p && p->nBytes + size < p->nSize) {
va_list jj;
va_start(jj, size); // get the address of the first argument
memcpy(p->args + p->nBytes, jj, size); // copy the specified size to the closure memory area.
p->nBytes += size; // keep up with how many total bytes we have copied
va_end(jj);
}
return p;
}
И сделать это немногоболее простой в использовании позволяет предоставить макрос обертки, который, как правило, удобен, но имеет ограничения, поскольку это манипулирование текстом процессора C.
#define PUSHDATA(cs,d) pushDataClosure((cs),sizeof(d),(d))
, поэтому мы могли бы использовать что-то вроде следующего исходного кода:
unsigned char closurearea[256];
int iValue = 34;
ClosureStruct *dd = PUSHDATA(beginClosure(z2func, 256, closurearea), iValue);
dd = PUSHDATA(dd, 68);
execClosure(dd);
Вызов замыкания: функция execClosure ()
Последним элементом этой функции является execClosure()
функция для выполнения функции замыкания с ее данными.В этой функции мы копируем список аргументов, предоставленный в структуре данных замыкания, в стек при вызове функции.
Мы приводим область аргументов данных замыкания к указателю наструктура, содержащая массив unsigned char
и разыменование указателя, чтобы компилятор C поместил копию аргументов в стек перед вызовом функции в замыкании.
Чтобы упростить созданиеexecClosure()
функция, мы создадим макрос, который позволит легко создавать различные размеры структур, которые нам нужны.
// helper macro to reduce type and reduce chance of typing errors.
#define CLOSEURESIZE(p,n) if ((p)->nBytes < (n)) { \
struct {\
unsigned char x[n];\
} *px = (void *)p->args;\
p->pf(*px);\
}
Затем мы используем этот макрос, чтобы создать серию тестов, чтобы определить, как вызыватьфункция закрытия.Выбранные здесь размеры могут нуждаться в настройке для конкретных применений.Эти размеры являются произвольными, и поскольку данные замыкания редко имеют одинаковый размер, это неэффективно использует пространство стека.И есть вероятность, что данных закрытия может быть больше, чем мы допустили.
// execute a closure by calling the function through the function pointer
// provided along with the created list of arguments.
ClosureStruct * execClosure(ClosureStruct *p)
{
if (p) {
// the following structs are used to allocate a specified size of
// memory on the stack which is then filled with a copy of the
// function argument list provided in the closure data.
CLOSEURESIZE(p,64)
else CLOSEURESIZE(p, 128)
else CLOSEURESIZE(p, 256)
else CLOSEURESIZE(p, 512)
else CLOSEURESIZE(p, 1024)
else CLOSEURESIZE(p, 1536)
else CLOSEURESIZE(p, 2048)
}
return p;
}
Мы возвращаем указатель на замыкание, чтобы сделать его легкодоступным.
Пример использования разработанной библиотеки
Мы можем использовать вышеизложенное следующим образом.Сначала пара примеров функций, которые на самом деле мало что делают.
int zFunc(int i, int j, int k)
{
printf("zFunc i = %d, j = %d, k = %d\n", i, j, k);
return i + j + k;
}
typedef struct { char xx[24]; } thing1;
int z2func(thing1 a, int i)
{
printf("i = %d, %s\n", i, a.xx);
return 0;
}
Затем мы строим наши замыкания и выполняем их.
{
unsigned char closurearea[256];
thing1 xpxp = { "1234567890123" };
thing1 *ypyp = &xpxp;
int iValue = 45;
ClosureStruct *dd = PUSHDATA(beginClosure(z2func, 256, malloc(256)), xpxp);
free(execClosure(PUSHDATA(dd, iValue)));
dd = PUSHDATA(beginClosure(z2func, 256, closurearea), *ypyp);
dd = PUSHDATA(dd, 68);
execClosure(dd);
dd = PUSHDATA(beginClosure(zFunc, 256, closurearea), iValue);
dd = PUSHDATA(dd, 145);
dd = PUSHDATA(dd, 185);
execClosure(dd);
}
, что дает вывод
i = 45, 1234567890123
i = 68, 1234567890123
zFunc i = 45, j = 145, k = 185
Ну, а как насчет каррирования?
Затем мы можем внести изменения в нашу структуру замыкания, чтобы мы могли выполнять каррирование функций.
typedef struct {
size_t nBytes; // current number of bytes of data
size_t nSize; // maximum size of the data area
size_t nCurry; // last saved nBytes for curry and additional arguments
void(*pf)(); // pointer to the function to invoke
unsigned char args[1]; // beginning of the data area for function arguments
} ClosureStruct;
с поддерживающими функциями для каррирования и сброса точки карри, являющимися
ClosureStruct *curryClosure(ClosureStruct *p)
{
p->nCurry = p->nBytes;
return p;
}
ClosureStruct *resetCurryClosure(ClosureStruct *p)
{
p->nBytes = p->nCurry;
return p;
}
Исходный код для тестирования это может быть:
{
unsigned char closurearea[256];
thing1 xpxp = { "1234567890123" };
thing1 *ypyp = &xpxp;
int iValue = 45;
ClosureStruct *dd = PUSHDATA(beginClosure(z2func, 256, malloc(256)), xpxp);
free(execClosure(PUSHDATA(dd, iValue)));
dd = PUSHDATA(beginClosure(z2func, 256, closurearea), *ypyp);
dd = PUSHDATA(dd, 68);
execClosure(dd);
dd = PUSHDATA(beginClosure(zFunc, 256, closurearea), iValue);
dd = PUSHDATA(dd, 145);
dd = curryClosure(dd);
dd = resetCurryClosure(execClosure(PUSHDATA(dd, 185)));
dd = resetCurryClosure(execClosure(PUSHDATA(dd, 295)));
}
с выводом
i = 45, 1234567890123
i = 68, 1234567890123
zFunc i = 45, j = 145, k = 185
zFunc i = 45, j = 145, k = 295