For fun I decided to try to emulate lambda functions in plain old C. It turns out it can be easily done with a bit of macro abuse.
CLambda
struct stores function pointer to the function and has its own "stack" for capturing local variables. Function signature is erased in the process and there is no type checking when calling it, so you can easily mess things up.
But other than that, I think it turned out quite nicely.
clambda.h
#ifndef CLAMBDA_H
#define CLAMBDA_H
#include <assert.h>
#include <stdint.h>
#include <string.h>
typedef struct CLambda CLambda;
typedef void (*CLambdaFn)(CLambda *lambda, ...);
#define CLAMBDA_STACK_SIZE 52
typedef struct CLambda {
CLambdaFn fn;
int32_t captureSize;
/* Captured variables */
uint8_t stack[CLAMBDA_STACK_SIZE];
} CLambda;
// static_assert(sizeof(CLambda) == 64, "");
#define CLAMBDA(lambdaName) \
(CLambda) { \
.fn = (CLambdaFn) lambdaName \
}
#define CLAMBDA_BEGIN_CAPTURE(lambda) \
do { \
CLambda *_lam = (lambda); \
_lam->captureSize = 0; \
} while (0)
#define CLAMBDA_CAPTURE(lambda, var) \
do { \
CLambda *_lam = (lambda); \
int32_t _stackSize = _lam->captureSize; \
int32_t _varSize = sizeof(var); \
assert(_stackSize + _varSize < sizeof(_lam->stack)); \
void *_location = &(var); \
memcpy(&_lam->stack[_stackSize], _location, _varSize); \
_lam->captureSize += _varSize; \
} while (0)
#define CLAMBDA_UNPACK_START() int32_t _stackOffset = 0
#define CLAMBDA_UNPACK(type, name) \
type name = (_stackOffset += sizeof(type), \
assert(_stackOffset <= _lam->captureSize), \
*(type *) &_lam->stack[_stackOffset - sizeof(type)])
#define CLAMBDA_CAST_RET(retType) (retType(*)(CLambda *, ...))
#define CLAMBDA_CALL(lambda, retType, ...) \
((CLAMBDA_CAST_RET(retType)((lambda)->fn))(lambda, ##__VA_ARGS__))
#define CLAMBDA_DECL(lambdaName, ...) \
lambdaName(CLambda *_lam, ##__VA_ARGS__)
#endif // CLAMBDA_H
main.c
#include <stdio.h>
#include "clambda.h"
int CLAMBDA_DECL(lambdaAdd, int a) {
CLAMBDA_UNPACK_START();
CLAMBDA_UNPACK(int, x); // still have access to x
return a + x;
}
void CLAMBDA_DECL(printArgs) {
CLAMBDA_UNPACK_START();
CLAMBDA_UNPACK(int, argc);
CLAMBDA_UNPACK(char **, argv);
printf("Program arguments:\n");
for (int i = 0; i < argc; i++) {
printf("\t%s\n", argv[i]);
}
}
CLambda foo(int x) {
CLambda lam = CLAMBDA(lambdaAdd);
CLAMBDA_BEGIN_CAPTURE(&lam);
CLAMBDA_CAPTURE(&lam, x);
// x goes out of scope
return lam;
}
int main(int argc, char **argv) {
CLambda lambda1 = foo(8);
printf("%d\n", CLAMBDA_CALL(&lambda1, int, 2));
CLambda printLam = CLAMBDA(printArgs);
CLAMBDA_BEGIN_CAPTURE(&printLam);
CLAMBDA_CAPTURE(&printLam, argc);
CLAMBDA_CAPTURE(&printLam, argv);
CLAMBDA_CALL(&printLam, void);
return 0;
}
To run it:
gcc -Wall -Wpedantic -std=c99 main.c -o lambda
./lambda
As a bonus, with GNU extensions you can also nest functions for a much nicer syntax, but at the cost of less portable code.
1 Answer 1
This is terrible, for several reasons.
- Lambdas are anonymous functions. Here nothing is anonymous.
- It's not type-safe. While C is not that great with type safety to begin with, this actually erases the type of the function, and you have to ensure you pass the correct return type and parameter types to
CLAMBDA_CALL()
. If you don't it will still compile, but result in unexpected behavior when running the program. The same goes for the captures. - The need to use lots of macros makes it look horrible.
I'm sorry to be so negative, but I think this is a case of: you should not want to do this. Either write what you want in plain C without using macros, which I think isn't even significantly more lines of code than what you have here in main.c, or use a programming language that does support lambdas, like C++.
-
2\$\begingroup\$ Yup. Right tool for the right job. Just because you can make a Turing machine do anything doesn’t mean you should tell it to do that. \$\endgroup\$J_H– J_H2024年02月07日 22:13:50 +00:00Commented Feb 7, 2024 at 22:13