source

C에서 카레를 할 수 있는 방법이 있습니까?

factcode 2023. 9. 26. 22:35
반응형

C에서 카레를 할 수 있는 방법이 있습니까?

합니다 에 대한 해 보겠습니다._stack_push(stack* stk, void* el). 나는 전화할 수 있기를 원합니다.curry(_stack_push, my_stack)다만 하면 수 .void* el않기 수 보다 훨씬 것을 C는 런타임 함수 정의를 허용하지 않기 때문에 방법을 생각할 수 없었지만 여기에는 나보다 훨씬 똑똑한 사람들이 있다는 것을 알고 있습니다 :).무슨 생각 있어요?

C/C++/Objective-C에서 카레에 대해 논하는 Laurent Dami의 논문을 발견했습니다.

C/C++/Objective-c에서 Curryed 기능으로 재사용성 향상

C에서 구현하는 방법에 관심이 있습니다.

우리의 현재 구현은 기존의 C 컨스트럭트를 사용하여 커리 메커니즘을 추가합니다.이것은 컴파일러를 수정하는 것보다 훨씬 쉬웠고, 카레의 흥미를 증명하기에 충분했습니다.그러나 이 방법은 두 가지 단점이 있습니다.첫째, 카레 함수는 형식 검사가 불가능하므로 오류를 방지하기 위해 신중한 사용이 필요합니다.둘째, 카레 함수는 인수의 크기를 알 수 없고, 인수가 정수 크기의 전부인 것처럼 계산합니다.

에는 되어 있지 .curry(), 그러나 함수 포인터와 가변 함수를 사용하여 어떻게 구현되는지 상상할 수 있습니다.

GCC는 중첩 함수의 정의에 대한 확장 기능을 제공합니다.이것은 ISO 표준 C는 아니지만 질문에 매우 편리하게 답할 수 있기 때문에 관심이 있을 수 있습니다.즉, 중첩 함수는 상위 함수 로컬 변수에 액세스할 수 있고 이에 대한 포인터도 상위 함수에 의해 반환될 수 있습니다.

다음은 간단한 자기 설명의 예입니다.

#include <stdio.h>

typedef int (*two_var_func) (int, int);
typedef int (*one_var_func) (int);

int add_int (int a, int b) {
    return a+b;
}

one_var_func partial (two_var_func f, int a) {
    int g (int b) {
        return f (a, b);
    }
    return g;
}

int main (void) {
    int a = 1;
    int b = 2;
    printf ("%d\n", add_int (a, b));
    printf ("%d\n", partial (add_int, a) (b));
}

하지만 이 공사에는 한계가 있습니다.에서와 같이 결과 함수에 대한 포인터를 유지하는 경우

one_var_func u = partial (add_int, a);

u(0) 것처럼치 않은 할 수 .다a떤.u되었습니다 되었습니다.partial

GCC의 문서의 이 섹션을 참조하십시오.

이것이 제 머리 속에 있는 첫 번째 추측입니다. (최선의 해결책은 아닐 수도 있습니다.)

카레 함수는 힙에서 일부 메모리를 할당하고 매개 변수 값을 힙 할당된 메모리에 넣을 수 있습니다.그러면 반환된 함수는 해당 힙 할당 메모리에서 매개 변수를 읽어야 한다는 것을 알게 됩니다.반환된 함수의 인스턴스가 하나뿐인 경우 해당 매개 변수에 대한 포인터를 단일 톤/글로벌에 저장할 수 있습니다.그렇지 않으면 반환된 함수의 인스턴스가 둘 이상인 경우, 카레는 힙 할당 메모리에 반환된 함수의 각 인스턴스를 만들어야 한다고 생각합니다("pointer to parameters", "push the parameters", "include that other function into the heap-allocated memory"와 같은 opcode를 기록함으로써).이 경우 할당된 메모리가 실행 가능한지 여부를 주의해야 하며 안티바이러스 프로그램을 두려워해야 합니다.

좋은 소식:컴파일러 고유의 기능을 사용하지 않고 표준 ANSIC에서 이를 수행할 프로그램을 작성하는 방법이 있습니다.(특히, 필요한 것은 아닙니다.gcc중첩 함수 지원입니다.)

나쁜 소식:실행 시 트램펄린 기능을 수행하려면 실행 코드의 작은 비트를 만들어야 합니다.이는 구현이 다음과 같이 결정된다는 것을 의미합니다.

  • 프로세서 명령어 집합
  • ABI(특히 함수 호출 규약)
  • 데이터를 실행 가능한 것으로 표시하는 운영 체제의 기능

가장 좋은 소식:실제 프로덕션 코드를 사용해야 하는 경우 의 클로저 API를 사용해야 합니다. 허가 라이센스가 부여되어 있으며 많은 플랫폼과 ABI에 대해 신중하고 유연한 구현이 포함되어 있습니다.


아직도 여기 계신다면, 이를 "처음부터" 구현하는 방법을 잘 알고 싶으실 것입니다.

아래 프로그램은 C에서 2-모수 함수를 1-모수 함수로 커리하는 방법을 보여줍니다.

  • x86-64 프로세서 아키텍처
  • 시스템 VABI
  • 리눅스 운영 체제

이는 감염성 실행 파일 스택의 "트램폴린 일러스트레이션"을 기반으로 하지만 에 트램폴린 구조가 저장되어 있습니다.malloc스택이 아닌 다른 위치로 이동합니다.가 없다는 것을 더 합니다가 .gcc -Wl,-z,execstack).

리눅스 시스템 호출을 사용하여 힙 개체를 실행할 수 있게 만듭니다.

2- 입니다)의입니다. ( 은 2- 입니다.uint32_t (*fp2)(uint32_t a, uint32_t b)및 하나의 합니다의 ( )로합니다.uint32_t (*fp1)(uint32_t a) 를를 .fp1에 대해 미리 합니다.b를 수행합니다 3-명령 트램펄린 기능을 작게 만들어 이렇게 할 수 있습니다.

movl $imm32, %esi  /* $imm32 filled in with the value of 'b' */
movq %imm64, %rax  /* $imm64 filled in with the value of 'fp2' */
jmpq *%rax

합니다.b그리고.fp2이 3할 수 .fp1상술한 바와 같이 정확하게그것은 하나의 매개 변수 함수가 첫 번째 매개 변수를 수신하는 x86-64 시스템 V 호출 규약을 준수하기 때문입니다.%edi/%rdi두고 2-파라미터 함수다를 합니다.%esi/%rsi등록하세요.이 경우 1-모수 트램펄린 함수는 다음 값을 받습니다.uint32_t 변수()%edi그런 두 인 를 합니다.uint32_t 변수()%esi그런 "합니다. 이 는 정확히 두 변수를 "" 2 합니다를 합니다. 합니다.

여기 완전한 작동 코드가 있습니다. 저는 또한 dlenski/c-c-curry에 GitHub에 게시했습니다.

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

#include <unistd.h>
#include <sys/mman.h>
#include <stdint.h>

#define PAGE_START(P) ((uintptr_t)(P) & ~(pagesize-1))
#define PAGE_END(P)   (((uintptr_t)(P) + pagesize - 1) & ~(pagesize-1))

/* x86-64 ABI passes parameters in rdi, rsi, rdx, rcx, r8, r9
 * (https://wiki.osdev.org/System_V_ABI), and return value
 * goes in %rax.
 *
 * Binary format of useful opcodes:
 *
 *       0xbf, [le32] = movl $imm32, %edi (1st param)
 *       0xbe, [le32] = movl $imm32, %esi (2nd param)
 *       0xba, [le32] = movl $imm32, %edx (3rd param)
 *       0xb9, [le32] = movl $imm32, %ecx (4rd param)
 *       0xb8, [le32] = movl $imm32, %eax
 * 0x48, 0x__, [le64] = movq $imm64, %r__
 *       0xff, 0xe0   = jmpq *%rax
 */

typedef uint32_t (*one_param_func_ptr)(uint32_t);
one_param_func_ptr curry_two_param_func(
    void *two_param_func, 
    uint32_t second_param)
{
    /* This is a template for calling a "curried" version of 
     * uint32_t (*two_param_func)(uint32_t a, uint32_t b),
     * using the Linux x86-64 ABI. The curried version can be
     * treated as uint32_t (*one_param_func)(uint32_t a).
     */
    uintptr_t fp = (uintptr_t)two_param_func;
    uint8_t template[] = {
        0xbe, 0, 0, 0, 0,                                   /* movl $imm32, %esi */
        0x48, 0xb8, fp >>  0, fp >>  8, fp >> 16, fp >> 24, /* movq fp, %rax */
                    fp >> 32, fp >> 40, fp >> 48, fp >> 56,
        0xff, 0xe0                                          /* jmpq *%rax */
    };
    
    /* Now we create a copy of this template on the HEAP, and
     * fill in the second param. */
    uint8_t *buf = malloc(sizeof(template));
    if (!buf)
        return NULL;
    
    memcpy(buf, template, sizeof(template));
    buf[1] = second_param >> 0;
    buf[2] = second_param >> 8;
    buf[3] = second_param >> 16;
    buf[4] = second_param >> 24;
    
    /* We do NOT want to make the stack executable,
     * but we NEED the heap-allocated buf to be executable.
     * Compiling with 'gcc -Wl,-z,execstack' would do BOTH.
     *
     * This appears to be the right way to only make a heap object executable:
     *   https://stackoverflow.com/a/23277109/20789
     */
    uintptr_t pagesize = sysconf(_SC_PAGE_SIZE);
    mprotect((void *)PAGE_START(buf),
             PAGE_END(buf + sizeof(template)) - PAGE_START(buf),
             PROT_READ|PROT_WRITE|PROT_EXEC);
    
    return (one_param_func_ptr)buf;
}

/********************************************/

int print_both_params(int a, int b)
{
    printf("Called with a=%d, b=%d\n", a, b);
    return a+b;
}

int main(int argc, char **argv)
{
    one_param_func_ptr print_both_params_b4 =
        curry_two_param_func(print_both_params, 4);
    one_param_func_ptr print_both_params_b256 = 
        curry_two_param_func(print_both_params, 256);
    
    print_both_params_b4(3);    // "Called with a=3, b=4"
    print_both_params_b256(6);  // "Called with a=6, b=256"

    return 0;
}

C는 런타임 함수 정의를 허용하지 않기 때문에

이것은 원칙적으로 표준 C에서는 사실입니다.자세한 내용은 n1570을 읽으십시오.

그러나 실제로는 거짓일 수 있습니다.고려하다

  • (예: Linux)에서 일부 임시 파일 POSIX 합니다(: Linux) 에서 C 를 생성합니다./tmp/generated1234.c합니다.void genfoo1234(void)함수, 그 파일을 컴파일하는 것 (를 들어 최근의 GCC 컴파일러를 사용하는 것)gcc -O -fPIC -Wall -shared /tmp/generated1234.c -o /tmp/generated1234.sodlopen(3)을(를) 사용합니다./tmp/generated1234.so다음에 dlsym(3) ongenfoo1234dlopen함수 포인터를 가져옵니다.개인적인 경험으로 볼 때, 그러한 접근 방식은 오늘날(2021년 Linux 노트북에서) 대화형 사용에도 충분히 빠릅니다(각각의 임시 생성된 C 파일에 수백 줄의 C 코드가 있는 경우).

  • x86, x86-64, GNU 라이트닝, libgccjit (또는 C++ asmjit)과 같은 기계 코드 생성 라이브러리를 사용하는 ARM 프로세서

실제로 닫힘 코드(닫힘 값으로 함수 포인터 그룹화)를 생성하여 콜백으로 사용합니다.

관련 사항은 쓰레기 수거이니, 쓰레기 수거 안내서를 읽어보세요.

Lua, GNU guile, Python기존의 인터프리터를 응용 프로그램에 내장하는 것도 고려해 보십시오.

적어도 영감을 얻기 위해 이 인터프리터들의 소스 코드를 연구합니다.

Quenniec의 책 Lisp의 작은 조각들Dragon 책은 읽을만한 가치가 있습니다.두 가지 모두 실제적인 문제와 구현 세부 사항을 설명합니다.

최근 GCC 컴파일러 (2021년) 참조

여기 C에서 카레를 하는 방법이 있습니다.이 샘플 애플리케이션은 편의를 위해 C++ iostream 출력을 사용하는 반면 모두 C 스타일 코딩입니다.

이 접근법의 핵심은 다음과 같은 것을 가지는 것입니다.struct그 안에는 다음과 같은 것들이 포함되어 있습니다.unsigned char그리고 이 배열은 함수에 대한 인수 목록을 작성하는 데 사용됩니다.호출할 함수는 배열에 푸시되는 인수 중 하나로 지정됩니다.그런 다음 결과 배열은 함수와 인수의 닫힘을 실제로 실행하는 프록시 함수에 주어집니다.

인 뿐만 넣는 몇합니다.pushMem()struct다른 메모리 영역을 선택할 수 있습니다.

이 방법은 폐쇄 데이터에 사용되는 메모리 영역을 할당해야 합니다.메모리 관리에 문제가 없도록 이 메모리 영역에 스택을 사용하는 것이 가장 좋습니다.또한, 폐쇄 저장 메모리 영역을 얼마나 크게 해야 필요한 인수를 위한 충분한 공간이 있지만 그렇게 크지 않아서 메모리 또는 스택의 초과 공간이 사용되지 않는 공간에 의해 차지되는 문제가 있습니다.

폐쇄 데이터를 저장하는 데 사용되는 현재 배열 크기에 대한 추가 필드를 포함하는 약간 다르게 정의된 폐쇄 구조를 사용하는 실험을 했습니다. 다른 폐쇄 된 도우미 다를 .unsigned char *폐쇄 구조체에 인수를 추가할 때 포인터를 사용합니다.

참고사항 및 주의사항

다음 예제 프로그램은 Visual Studio 2013을 통해 컴파일되고 테스트되었습니다.이 샘플의 출력은 아래와 같습니다.이 예제에서 GCC나 CLANG을 사용하는 것에 대해 확신할 수 없습니다. 또한 64비트 컴파일러에서 볼 수 있는 문제에 대해서도 확신할 수 없습니다. 제 테스트가 32비트 응용 프로그램에서 이루어졌다는 인상을 받고 있기 때문입니다. 이 방식은 후를 터뜨리는 것을 C하는 것처럼 준 C다).__cdecl 안 돼요.__stdcall(Windows API 에서).

런타임에 인수 목록을 작성한 다음 프록시 함수를 호출하기 때문에 이 접근 방식은 컴파일러가 인수에 대한 검사를 수행할 수 없습니다.이로 인해 컴파일러가 플래그를 지정할 수 없는 일치하지 않는 매개 변수 유형으로 인해 알 수 없는 오류가 발생할 수 있습니다.

예시적용

// currytest.cpp : Defines the entry point for the console application.
//
// while this is C++ usng the standard C++ I/O it is written in
// a C style so as to demonstrate use of currying with C.
//
// this example shows implementing a closure with C function pointers
// along with arguments of various kinds. the closure is then used
// to provide a saved state which is used with other functions.

#include "stdafx.h"
#include <iostream>

// notation is used in the following defines
//   - tname is used to represent type name for a type
//   - cname is used to represent the closure type name that was defined
//   - fname is used to represent the function name

#define CLOSURE_MEM(tname,size) \
    typedef struct { \
        union { \
            void *p; \
            unsigned char args[size + sizeof(void *)]; \
        }; \
    } tname;

#define CLOSURE_ARGS(x,cname) *(cname *)(((x).args) + sizeof(void *))
#define CLOSURE_FTYPE(tname,m) ((tname((*)(...)))(m).p)

// define a call function that calls specified function, fname,
// that returns a value of type tname using the specified closure
// type of cname.
#define CLOSURE_FUNC(fname, tname, cname) \
    tname fname (cname m) \
    { \
        return ((tname((*)(...)))m.p)(CLOSURE_ARGS(m,cname)); \
    }

// helper functions that are used to build the closure.
unsigned char * pushPtr(unsigned char *pDest, void *ptr) {
    *(void * *)pDest = ptr;
    return pDest + sizeof(void *);
}

unsigned char * pushInt(unsigned char *pDest, int i) {
    *(int *)pDest = i;
    return pDest + sizeof(int);
}

unsigned char * pushFloat(unsigned char *pDest, float f) {
    *(float *)pDest = f;
    return pDest + sizeof(float);
}

unsigned char * pushMem(unsigned char *pDest, void *p, size_t nBytes) {
    memcpy(pDest, p, nBytes);
    return pDest + nBytes;
}


// test functions that show they are called and have arguments.
int func1(int i, int j) {
    std::cout << " func1 " << i << " " << j;
    return i + 2;
}

int func2(int i) {
    std::cout << " func2 " << i;
    return i + 3;
}

float func3(float f) {
    std::cout << " func3 " << f;
    return f + 2.0;
}

float func4(float f) {
    std::cout << " func4 " << f;
    return f + 3.0;
}

typedef struct {
    int i;
    char *xc;
} XStruct;

int func21(XStruct m) {
    std::cout << " fun21 " << m.i << " " << m.xc << ";";
    return m.i + 10;
}

int func22(XStruct *m) {
    std::cout << " fun22 " << m->i << " " << m->xc << ";";
    return m->i + 10;
}

void func33(int i, int j) {
    std::cout << " func33 " << i << " " << j;
}

// define my closure memory type along with the function(s) using it.

CLOSURE_MEM(XClosure2, 256)           // closure memory
CLOSURE_FUNC(doit, int, XClosure2)    // closure execution for return int
CLOSURE_FUNC(doitf, float, XClosure2) // closure execution for return float
CLOSURE_FUNC(doitv, void, XClosure2)  // closure execution for void

// a function that accepts a closure, adds additional arguments and
// then calls the function that is saved as part of the closure.
int doitargs(XClosure2 *m, unsigned char *x, int a1, int a2) {
    x = pushInt(x, a1);
    x = pushInt(x, a2);
    return CLOSURE_FTYPE(int, *m)(CLOSURE_ARGS(*m, XClosure2));
}

int _tmain(int argc, _TCHAR* argv[])
{
    int k = func2(func1(3, 23));
    std::cout << " main (" << __LINE__ << ") " << k << std::endl;

    XClosure2 myClosure;
    unsigned char *x;

    x = myClosure.args;
    x = pushPtr(x, func1);
    x = pushInt(x, 4);
    x = pushInt(x, 20);
    k = func2(doit(myClosure));
    std::cout << " main (" << __LINE__ << ") " << k << std::endl;

    x = myClosure.args;
    x = pushPtr(x, func1);
    x = pushInt(x, 4);
    pushInt(x, 24);               // call with second arg 24
    k = func2(doit(myClosure));   // first call with closure
    std::cout << " main (" << __LINE__ << ") " << k << std::endl;
    pushInt(x, 14);              // call with second arg now 14 not 24
    k = func2(doit(myClosure));  // second call with closure, different value
    std::cout << " main (" << __LINE__ << ") " << k << std::endl;

    k = func2(doitargs(&myClosure, x, 16, 0));  // second call with closure, different value
    std::cout << " main (" << __LINE__ << ") " << k << std::endl;

    // further explorations of other argument types

    XStruct xs;

    xs.i = 8;
    xs.xc = "take 1";
    x = myClosure.args;
    x = pushPtr(x, func21);
    x = pushMem(x, &xs, sizeof(xs));
    k = func2(doit(myClosure));
    std::cout << " main (" << __LINE__ << ") " << k << std::endl;

    xs.i = 11;
    xs.xc = "take 2";
    x = myClosure.args;
    x = pushPtr(x, func22);
    x = pushPtr(x, &xs);
    k = func2(doit(myClosure));
    std::cout << " main (" << __LINE__ << ") " << k << std::endl;

    x = myClosure.args;
    x = pushPtr(x, func3);
    x = pushFloat(x, 4.0);

    float dof = func4(doitf(myClosure));
    std::cout << " main (" << __LINE__ << ") " << dof << std::endl;

    x = myClosure.args;
    x = pushPtr(x, func33);
    x = pushInt(x, 6);
    x = pushInt(x, 26);
    doitv(myClosure);
    std::cout << " main (" << __LINE__ << ") " << std::endl;

    return 0;
}

시험출력

이 샘플 프로그램에서 출력합니다.괄호 안의 숫자는 함수 호출이 이루어지는 메인의 라인 번호입니다.

 func1 3 23 func2 5 main (118) 8
 func1 4 20 func2 6 main (128) 9
 func1 4 24 func2 6 main (135) 9
 func1 4 14 func2 6 main (138) 9
 func1 4 16 func2 6 main (141) 9
 fun21 8 take 1; func2 18 main (153) 21
 fun22 11 take 2; func2 21 main (161) 24
 func3 4 func4 6 main (168) 9
 func33 6 26 main (175)

언급URL : https://stackoverflow.com/questions/1023261/is-there-a-way-to-do-currying-in-c

반응형