Prolog에서 C 호출

Prolog에서 C 호출

Prolog 술어에서 C 함수를 호출하는 방법을 설명해요.

출처: GNU Prolog Manual - Calling C from Prolog

본문

소개

이 인터페이스는 간단한 C 루틴과 복잡한 C 루틴을 모두 작성하는 데 사용될 수 있어요. 간단한 루틴은 유형이 단순한 입력 또는 출력 인자만 사용해요. 그 경우 모든 Prolog ↔ C 데이터 변환이 암시적으로 이루어지므로 사용자는 Prolog 데이터 구조에 대한 지식이 필요 없어요. 복잡한 항(리스트, 구조체)을 조작하려면 함수 집합이 제공돼요. 마지막으로 비결정적 C 코드를 작성하는 것도 가능해요.

foreign/2 지시자

foreign/2 지시자(7.1.15절)는 C 함수 인터페이스를 선언해요. 일반적인 형태는 foreign(Template, Options)이며, 주어진 옵션에 따라 프로토타입이 Template인 인터페이스 술어를 정의해요. Template은 연관된 Prolog 술어의 각 인자의 유형/모드를 지정하는 callable 항이에요.

foreign 옵션: Options는 foreign 옵션 리스트예요. 이 리스트가 모순된 옵션을 포함하면 가장 오른쪽 옵션이 적용돼요. 가능한 옵션은:

  • fct_name(F) — F가 호출할 C 함수의 이름을 나타내는 원자. 기본적으로 C 함수의 이름은 Template의 주 functor와 같아요. 어쨌든 함수 이름과 연관된 원자는 C 식별자 구문을 준수해야 해요.
  • return(boolean / none / jump) — C 함수가 반환하는 값을 지정:
    • boolean — 함수 유형이 PlBool(성공 시 PL_TRUE, 그 외에는 PL_FALSE 반환).
    • none — 함수 유형이 void(반환 값 없음).
    • jump — 함수 유형이 void(*)( )(실행할 Prolog 코드의 주소를 반환). 기본값은 boolean이에요.
  • bip_name(Name, Arity) — Name과 Arity로 오류 컨텍스트를 초기화. 오류가 발생하면 이 정보가 어느 술어에서 오류가 발생했는지 나타내는 데 사용돼요(6.3.1절). bip_name(none)을 사용해 오류 컨텍스트의 초기화를 방지하는 것도 가능해요. 기본적으로 Name과 Arity는 Template의 functor와 항수로 설정돼요.
  • choice_size(N) — 이 옵션은 함수가 비결정적 코드를 구현한다고 지정. N은 비결정적 C 함수가 필요로 하는 크기를 지정하는 정수. 이 기능은 나중에 설명돼요(10.3.7절). 기본적으로 foreign 함수는 결정적이에요.

foreign(Template)foreign(Template, [])과 동일해요.

foreign 모드와 유형: Template의 각 인자는 대응하는 인자의 foreign 모드와 유형을 지정해요. 이 정보는 실행 시 유효 인자의 유형을 확인하고 Prolog ↔ C 데이터 변환을 수행하는 데 사용돼요. Template의 각 인자는 모드 기호 뒤에 유형 이름이 붙어 형성돼요. 가능한 foreign 모드는:

  • + — 입력 인자.
  • - — 출력 인자.
  • ? — 입출력 인자.

가능한 foreign 유형:

Foreign 유형 Prolog 유형 C 유형 C 유형 설명
integer 정수 PlLong 정수의 값
positive 양의 정수 PlLong 정수의 값
float 부동소수점 수 double 부동소수점 수의 값
number 숫자 double 숫자의 값
atom 원자 PlLong 원자의 내부 키
boolean 불리언 PlLong 불리언의 값(0=거짓, 1=참)
char 문자 PlLong (문자 코드의) 값
code 문자 코드 PlLong 문자 코드의 값
byte 바이트 PlLong 바이트의 값
in_char in-문자 PlLong 문자 값 또는 파일 끝에 대해 -1
in_code in-문자 코드 PlLong 문자 코드 값 또는 파일 끝에 대해 -1
in_byte in-바이트 PlLong 바이트 값 또는 파일 끝에 대해 -1
string 원자 char * 원자 이름을 포함하는 C 문자열
chars 문자 리스트 char * 리스트의 문자들을 포함하는 C 문자열
codes 문자 코드 리스트 char * 리스트의 문자들을 포함하는 C 문자열
term Prolog 항 PlTerm 일반적인 Prolog 항

단순 foreign 유형: 단순 유형은 위 표에 나열된 term을 제외한 모든 foreign 유형이에요. 단순 foreign 유형은 원자 항이에요(문자 및 문자 코드 리스트는 사실 상수 리스트). 각 단순 foreign 유형은 C 함수 작성을 단순화하기 위해 C 유형으로/로부터 변환돼요.

복합 foreign 유형: term foreign 유형은 어떤 Prolog 항(예: 리스트, 구조체 등)을 가리켜요. 그런 유형이 지정되면 인자는 PlTerm(PlLong과 동일한 GNU Prolog C 유형)으로 C 함수에 전달돼요. PlTerm 변수를 조작하는 여러 함수가 제공돼요(10.4절). 원래 항이 함수에 전달되므로 그 값을 읽거나 통일할 수 있어요. 그래서 모드 기호의 의미는 덜 중요해요. 이런 이유로 모드 기호를 생략하는 것이 가능해요. 그 경우 term은 +term과 동일해요.

C 함수

C 함수가 반환하는 유형은 return foreign 옵션의 값에 따라 달라져요(10.3.2절). 그것이 boolean이면 C 함수는 유형 PlBool이며 성공 시 PL_TRUE, 그 외에는 PL_FALSE를 반환해야 해요. return 옵션이 none이면 C 함수는 유형 void예요. 마지막으로 jump이면 함수는 Prolog 술어의 주소를 반환해야 하고, 함수가 끝나면 제어가 그 술어에 주어져요.

C 함수 인자의 유형은 Template에 지정된 대응 인자의 모드와 유형 선언에 따라 달라져요(다음 절들에서 설명).

입력 인자

입력 인자는 실행 시 테스트되어 그 유형이 foreign 유형에 맞는지 확인된 다음 C 함수에 전달돼요. 연관된 C 인자의 유형은 위 표(10.3.2절)가 주어요. 예를 들어 +positive foreign 선언과 연관된 유효 인자 Arg는 다음 과정을 거쳐요:

  • Arg가 변수이면 instantiation_error가 발생.
  • Arg가 변수도 정수도 아니면 type_error(integer, Arg)가 발생.
  • Arg가 0보다 작은 정수이면 domain_error(not_less_than_zero, Arg)가 발생.
  • 그 외에는 Arg의 값이 C 함수에 정수(PlLong)로 전달.

+string이 지정되면 함수에 전달된 문자열은 대응하는 원자의 내부 문자열이며 수정해서는 안 돼요. +term이 지정되면 함수에 전달된 항은 원래 Prolog 항이에요. 읽고/통일될 수 있어요. 모드 기호 없이 term이 지정될 때도 마찬가지예요.

출력 인자

출력 인자는 실행 시 테스트되어 그 유형이 foreign 유형에 맞는지 확인된 다음 C 함수가 설정한 값과 통일돼요. 연관된 C 인자의 유형은 위 표(10.3.2절)가 주는 유형에 대한 포인터예요. 예를 들어 -positive foreign 선언과 연관된 유효 인자 Arg는 다음과 같이 처리돼요:

  • Arg가 변수도 정수도 아니면 type_error(integer, Arg)가 발생.
  • Arg가 0보다 작은 정수이면 domain_error(not_less_than_zero, Arg)가 발생.
  • 그 외에는 정수에 대한 포인터(PlLong *)가 C 함수에 전달. 함수가 PL_TRUE를 반환하면 이 위치에 저장된 정수가 Arg와 통일.

-term이 지정되면 함수는 그 대응 인자(유형 PlTerm *)에 항을 구성해야 해요. 함수가 끝나면 이 항이 실제 술어 인자와 통일돼요.

입출력 인자

기본적으로 입출력 인자는 변수가 아니면 입력 인자로, 변수이면 출력 인자로 처리돼요. 연관된 C 인자의 유형은 다음과 같이 정의된 PlFIOArg(GNU Prolog C 유형)에 대한 포인터예요:

typedef struct
{
    PlBool is_var;
    PlBool unify;
    union
    {
        PlLong l;
        char *s;
        double d;
    } value;
} PlFIOArg;
  • is_var 필드는 인자가 변수이면 PL_TRUE, 그 외에는 PL_FALSE로 설정돼요. 이 값은 C 함수가 어떤 처리를 수행할지 결정하기 위해 테스트할 수 있어요.
  • unify 필드는 유효 인자가 C 함수 끝에서 통일되어야 하는지 제어해요. 초기에 unify는 is_var와 같은 값으로 설정되지만(즉 변수 인자는 통일되고 비변수 인자는 그렇지 않음) C 함수가 수정할 수 있어요.
  • value 필드는 인자의 값을 저장해요. 값 유형이 여러 종류가 있으므로 C 공용체로 선언돼요. s 필드는 C 문자열에, d는 C double에, 그 외에는 l에(int, PlLong, PlTerm) 사용돼요.

is_var가 PL_FALSE이면 value는 입력 인자와 같은 관례(10.3.4절)로 인자의 입력 값을 포함해요. 함수 끝에서 unify가 PL_TRUE이면 value는 출력 인자와 같은 관례(10.3.5절)로 통일할 값을 포함해야 해요.

예를 들어 ?positive foreign 선언과 연관된 유효 인자 Arg는 다음과 같이 처리돼요:

  • Arg가 변수이면 is_var와 unify가 PL_TRUE로 설정, 그 외에는 PL_FALSE로 설정되고 그 값이 value.l에 복사.
  • Arg가 변수도 정수도 아니면 type_error(integer, Arg)가 발생.
  • Arg가 0보다 작은 정수이면 domain_error(not_less_than_zero, Arg)가 발생.
  • 그 외에는 PlFIOArg(PlFIOArg *)에 대한 포인터가 C 함수에 전달. 함수가 PL_TRUE를 반환하고 unify가 PL_TRUE이면 value.l에 저장된 값이 Arg와 통일.

비결정적 C 코드 작성

인터페이스를 사용하면 비결정적 C 코드를 작성할 수 있어요. C 함수가 비결정적일 때 그 함수에 선택점(choice-point)이 생성돼요. 실패가 발생하면 더 최근의 비결정적 코드가 모두 끝났을 때 함수가 다시 호출돼요. 따라서 비결정적 코드에 더 이상 해(즉 선택)가 없을 때 Prolog에 알리는 것이 중요해요. 그래서 선택이 더 이상 남지 않으면 함수는 선택점을 제거해야 해요.

인터페이스는 함수가 다시 호출될 때마다 카운터를 증가시켜요. 첫 호출에서 이 카운터는 0이에요. 이 정보는 함수가 자신의 첫 호출을 감지할 수 있게 해줘요. 비결정적 코드를 작성할 때 함수의 연속적인 재호출 사이에 데이터를 기록하는 것이 종종 유용해요. 인터페이스는 그런 정보를 기록하는 버퍼를 유지해요. 이 버퍼의 크기는 foreign/2(10.3.2절)를 사용할 때 choice_size(N)이 줘요. 이 크기는 C 함수가 필요로 하는 (연속된) PlLong의 수예요.

함수 안에서 다음 함수/매크로를 호출할 수 있어요:

  • int Pl_Get_Choice_Counter(void) — 호출 카운터의 값을 반환(첫 호출에서 0).
  • TYPE Pl_Get_Choice_Buffer(TYPE) — 버퍼에 대한 포인터를 반환(TYPE으로 캐스팅).
  • void Pl_No_More_Choice(void) — 함수와 연관된 선택점을 삭제.

예제: 입력 및 출력 인자

여기에 제시된 모든 예제는 배포판의 ExamplesC 하위 디렉토리, 파일 examp.pl(Prolog 부분)과 examp_c.c(C 부분)에서 찾을 수 있어요.

원자 A에서 문자 C의 첫 번째 발생의 위치(0부터)를 P와 통일하는 술어 first_occurrence(A, C, P)를 정의해 봅시다. C가 A에 없으면 술어는 실패해야 해요.

Prolog 파일 examp.pl:

:- foreign(first_occurrence(+string, +char, -positive)).

C 파일 examp_c.c:

#include <string.h>
#include <gprolog.h>

PlBool
first_occurrence(char *str, PlLong c, PlLong *pos)
{
    char *p;
    p = strchr(str, c);
    if (p == NULL) /* C does not appear in A */
        return PL_FALSE; /* fail */
    *pos = p - str; /* set the output argument */
    return PL_TRUE; /* succeed */
}

컴파일은 examp라는 실행 파일을 만듭니다:

% gplc examp.pl examp_c.c

사용 예:

| ?- first_occurrence(prolog, p, X).
X = 0
| ?- first_occurrence(prolog, k, X).
no
| ?- first_occurrence(prolog, A, X).
{exception: error(instantiation_error,first_occurrence/3)}
| ?- first_occurrence(prolog, 1, X).
{exception: error(type_error(character,1),first_occurrence/3)}

예제: 비결정적 코드

원자 A에서 문자 C의 한 발생의 위치(0부터)를 P와 통일하는 술어 occurrence(A, C, P)를 정의해 봅시다. C가 A에 없으면 술어는 실패해요. 술어는 백트래킹 시 재실행 가능해요. 함수의 두 호출 사이에 기록되어야 하는 정보는 A에서 C를 검색할 다음 시작 위치예요.

Prolog 파일 examp.pl:

:- foreign(occurrence(+string, +char, -positive),
           [choice_size(1)]).

C 파일 examp_c.c:

#include <string.h>
#include <gprolog.h>

PlBool
occurrence(char *str, PlLong c, PlLong *pos)
{
    char **info_pos;
    char *p;
    info_pos = Pl_Get_Choice_Buffer(char **); /* recover the buffer */
    if (Pl_Get_Choice_Counter() == 0) /* first invocation ? */
        *info_pos = str;
    p = strchr(*info_pos, c);
    if (p == NULL) /* c does not appear */
    {
        Pl_No_More_Choice(); /* remove choice-point */
        return PL_FALSE; /* fail */
    }
    *pos = p - str; /* set the output argument */
    *info_pos = p + 1; /* update next starting pos */
    return PL_TRUE; /* succeed */
}

사용 예:

| ?- occurrence(prolog, o, X).
X = 2 ? (여기서 사용자가 ;를 눌러 다른 해를 계산)
X = 4 ? (여기서 사용자가 ;를 눌러 다른 해를 계산)
no (더 이상 해 없음)
| ?- occurrence(prolog, k, X).
no

첫 예에서 두 번째(마지막) 발생을 찾을 때(X=4) 선택점이 남아 있고 실패는 다른 해가 요청될(;) 때만 감지돼요. 더 이상 발생이 없을 때 선택점을 삭제해 이 동작을 개선할 수 있어요. 이를 위해 한 번 앞서 검색해야 해요. 저장된 정보는 다음 발생의 위치예요. occurrence2/3 술어에 대해 그런 동작을 정의해 봅시다.

Prolog 파일 examp.pl:

:- foreign(occurrence2(+string, +char, -positive),
           [choice_size(1)]).

C 파일 examp_c.c:

#include <string.h>
#include <gprolog.h>

PlBool
occurrence2(char *str, PlLong c, PlLong *pos)
{
    char **info_pos;
    char *p;
    info_pos = Pl_Get_Choice_Buffer(char **); /* recover the buffer */
    if (Pl_Get_Choice_Counter() == 0) /* first invocation ? */
    {
        p = strchr(str, c);
        if (p == NULL) /* C does not appear at all */
        {
            Pl_No_More_Choice(); /* remove choice-point */
            return PL_FALSE; /* fail */
        }
        *info_pos = p;
    }
    /* info_pos = an occurrence */
    *pos = *info_pos - str; /* set the output argument */
    p = strchr(*info_pos + 1, c);
    if (p == NULL) /* no more occurrence */
        Pl_No_More_Choice(); /* remove choice-point */
    else
        *info_pos = p; /* else update next solution */
    return PL_TRUE; /* succeed */
}

사용 예:

| ?- occurrence2(prolog, l, X).
X = 3 (더 이상 대안이 없으므로 사용자에게 묻지 않음)
| ?- occurrence2(prolog, o, X).
X = 2 ? (여기서 사용자가 ;를 눌러 다른 해를 계산)
X = 4 (더 이상 대안이 없으므로 사용자에게 묻지 않음)

예제: 입출력 인자

문자 Char와 그 문자 코드 Code를 양방향으로 변환하는 술어 char_ascii(Char, Code)를 정의해 봅시다. 이 술어는 char_code/2(8.19.4절)와 비슷해요.

Prolog 파일 examp.pl:

:- foreign(char_ascii(?char, ?code)).

C 파일 examp_c.c:

#include <gprolog.h>

PlBool
char_ascii(PlFIOArg *c, PlFIOArg *ascii)
{
    if (!c->is_var) /* Char is not a variable */
    {
        ascii->unify = PL_TRUE; /* enforce unif. of Code */
        ascii->value.l = c->value.l; /* set Code */
        return PL_TRUE; /* succeed */
    }
    if (ascii->is_var) /* Code is also a variable */
        Pl_Err_Instantiation(); /* emit instantiation_error */
    c->value.l = ascii->value.l; /* set Char */
    return PL_TRUE; /* succeed */
}

Char가 인스턴스화되면 Code의 통일을 강제해야 해요(그것이 인스턴스화될 수 있으므로). 입출력 인자가 인스턴스화되면 함수 끝에서 기본적으로 통일되지 않는다는 점을 기억해요(10.3.6절). Char와 Code가 모두 변수이면 함수는 instantiation_error를 발생시켜요. Prolog 오류를 발생시키는 방법은 나중에 설명돼요(10.5절).

사용 예:

| ?- char_ascii(a, X).
X = 97
| ?- char_ascii(X, 65).
X = 'A'
| ?- char_ascii(a, 12).
no
| ?- char_ascii(X, X).
{exception: error(instantiation_error,char_ascii/2)}
| ?- char_ascii(1, 12).
{exception: error(type_error(character,1),char_ascii/2)}

더 알아보기