타입 세이프 구조체 포인터 대신 퍼블릭 API로 캐스팅해야하는 불투명 한 “핸들”을 사용해야하는 이유는 무엇입니까? en = malloc(sizeof(*en));

공용 API가 현재 다음과 같은 라이브러리를 평가하고 있습니다.

libengine.h

/* Handle, used for all APIs */
typedef size_t enh;


/* Create new engine instance; result returned in handle */
int en_open(int mode, enh *handle);

/* Start an engine */
int en_start(enh handle);

/* Add a new hook to the engine; hook handle returned in h2 */
int en_add_hook(enh handle, int hooknum, enh *h2);

참고 enh여러 가지 데이터 유형 (핸들로 사용되는 일반적인 핸들, 엔진후크 ).

내부적으로 이러한 API의 대부분은 “핸들”을 내부 구조에 캐스트했습니다 malloc.

engine.c

struct engine
{
    // ... implementation details ...
};

int en_open(int mode, *enh handle)
{
    struct engine *en;

    en = malloc(sizeof(*en));
    if (!en)
        return -1;

    // ...initialization...

    *handle = (enh)en;
    return 0;
}

int en_start(enh handle)
{
    struct engine *en = (struct engine*)handle;

    return en->start(en);
}

개인적으로, typedef특히 타입 안전성이 떨어질 때 뒤에 숨어있는 것을 싫어 합니다. ( enh, 실제로 참조하는 내용을 어떻게 알 수 있습니까?)

그래서 풀 요청을 제출하여 다음과 같은 API 변경을 제안했습니다 ( 전체 라이브러리를 수정 한 후 ).

libengine.h

struct engine;           /* Forward declaration */
typedef size_t hook_h;    /* Still a handle, for other reasons */


/* Create new engine instance, result returned in en */
int en_open(int mode, struct engine **en);

/* Start an engine */
int en_start(struct engine *en);

/* Add a new hook to the engine; hook handle returned in hh */
int en_add_hook(struct engine *en, int hooknum, hook_h *hh);

물론 이렇게하면 내부 API 구현이 훨씬 개선되어 캐스트가 제거되고 소비자의 관점에서 유형 안전성이 유지됩니다.

libengine.c

struct engine
{
    // ... implementation details ...
};

int en_open(int mode, struct engine **en)
{
    struct engine *_e;

    _e = malloc(sizeof(*_e));
    if (!_e)
        return -1;

    // ...initialization...

    *en = _e;
    return 0;
}

int en_start(struct engine *en)
{
    return en->start(en);
}

다음과 같은 이유로 이것을 선호합니다.

그러나 프로젝트의 소유자는 풀 요청에서 멈췄습니다 (그림으로 표시).

개인적으로 나는을 노출시키는 아이디어를 좋아하지 않습니다 struct engine. 나는 아직도 현재의 방법이 더 깨끗하고 친절하다고 생각합니다.

처음에는 후크 핸들에 다른 데이터 유형을 사용했지만으로 전환하기로 결정 enh했기 때문에 모든 종류의 핸들이 동일한 데이터 유형을 공유하여 단순하게 유지했습니다. 이것이 혼란스러운 경우 다른 데이터 형식을 사용할 수 있습니다.

다른 사람들이이 PR에 대해 어떻게 생각하는지 봅시다.

이 라이브러리는 현재 비공개 베타 단계이므로 걱정할 소비자 코드가 많지 않습니다. 또한 이름을 약간 난독 화했습니다.


명명 된 불투명 구조체보다 불투명 핸들이 더 나은 방법은 무엇입니까?

참고 :이 질문은 Code Review 에서 닫혔습니다.



답변

“간단한 것이 더 낫다”만트라가 너무 독단적으로되었다. 단순한 것이 다른 것들을 복잡하게 만드는 것이 항상 좋은 것은 아닙니다. 어셈블리는 간단합니다. 각 명령은 고급 언어 명령보다 훨씬 간단하지만 어셈블리 프로그램은 동일한 작업을 수행하는 고급 언어보다 복잡합니다. 귀하의 경우, 균일 한 핸들 유형 enh은 기능을 복잡하게 만드는 대신 유형을 단순화합니다. 일반적으로 프로젝트 유형은 함수에 비해 하위 선형 비율로 증가하는 경향이 있기 때문에 프로젝트가 커질수록 일반적으로 함수를 더 단순하게 만들 수있는 경우 더 복잡한 유형을 선호합니다.

프로젝트 작성자는 귀하의 접근 방식이 ” 노출struct engine “인 것으로 우려하고 있습니다 . 나는 그들에게 구조체 자체를 노출시키는 것이 아니라 라는 이름의 구조체 가 있다는 사실 만을 설명했을 것입니다 engine. 라이브러리 사용자는 이미 해당 유형을 알고 있어야합니다. 예를 들어, 첫 번째 인수 en_add_hook가 해당 유형이고 첫 번째 인수가 다른 유형임을 알아야합니다. 따라서 함수의 “서명”문서에 이러한 유형을 문서화하는 대신 다른 곳에 문서화해야하고 컴파일러가 더 이상 프로그래머의 유형을 확인할 수 없기 때문에 실제로 API를 더 복잡하게 만듭니다.

한 가지 주목할 점-새 API는 작성하는 대신 사용자 코드를 좀 더 복잡하게 만듭니다.

enh en;
en_open(ENGINE_MODE_1, &en);

이제 핸들을 선언하려면 더 복잡한 구문이 필요합니다.

struct engine* en;
en_open(ENGINE_MODE_1, &en);

그러나 해결책은 매우 간단합니다.

struct _engine;
typedef struct _engine* engine

이제 다음과 같이 간단하게 작성할 수 있습니다.

engine en;
en_open(ENGINE_MODE_1, &en);

답변

여기 양쪽에 혼란이있는 것 같습니다.

  • 핸들 방식을 사용하면 모든 핸들에 단일 핸들 유형을 사용할 필요가 없습니다.
  • struct이름을 노출 해도 세부 정보가 노출되지 않습니다 (단지 존재)

C와 같은 언어에서 포인터를 사용하면 포인트를 직접 조작 할 수 있기 때문에 (호출을 포함하여 free) 핸들을 전달하면 클라이언트가 API를 통해 작업을 수행해야 하므로 C와 같은 언어에서 핸들을 사용하는 것보다 핸들을 사용하는 경우 이점이 있습니다. .

그러나 a를 통해 정의 된 단일 유형의 핸들을 갖는 방법은 typedef유형이 안전하지 않으며 많은 슬픔을 유발할 수 있습니다.

따라서 제 개인적인 제안은 타입 안전 핸들을 향해 나아가는 것입니다. 이것은 오히려 간단하게 달성됩니다.

typedef struct {
    size_t id;
} enh;

typedef struct {
    size_t id;
} oth;

이제 2실수로 핸들을 지나갈 수 없으며 엔진 핸들이 예상되는 빗자루에 실수로 핸들을 전달할 수도 없습니다 .


그래서 풀 요청을 제출하여 다음 API 변경을 제안합니다 ( 전체 라이브러리를 수정 한 후 )

그것은 당신의 실수입니다 : 오픈 소스 라이브러리에 대한 중요한 연구에 참여하기 전에, 저자 / 관리자에게 연락하여 변경 사항을 미리 논의하십시오 . 이를 통해 두 사람은 무엇을해야하는지 (또는하지 말아야 할)에 동의하고 불필요한 작업과 그로 인한 좌절을 피할 수 있습니다.


답변

불투명 핸들이 필요한 상황은 다음과 같습니다.

struct SimpleEngine {
    int type;  // always SimpleEngine.type = 1
    int a;
};

struct ComplexEngine {
    int type;  // always ComplexEngine.type = 2
    int a, b, c;
};

int en_start(enh handle) {
    switch(*(int*)handle) {
    case 1:
        // treat handle as SimpleEngine
        return start_simple_engine(handle);
    case 2:
        // treat handle as ComplexEngine
        return start_complex_engine(handle);
    }
}

라이브러리에 위의 “type”과 같이 필드의 헤더 부분이 동일한 두 개 이상의 구조체 유형이있는 경우 이러한 구조체 유형은 공통 부모 구조체 (C ++의 기본 클래스와 같은)를 갖는 것으로 간주 될 수 있습니다.

헤더 부분을 다음과 같이 “struct engine”으로 정의 할 수 있습니다.

struct engine {
    int type;
};

struct SimpleEngine {
    struct engine base;
    int a;
};

struct ComplexEngine {
    struct engine base;
    int a, b, c;
};

int en_start(struct engine *en) { ... }

그러나 구조체 엔진 사용 여부에 관계없이 형식 캐스트가 필요하기 때문에 선택 사항입니다.

결론

어떤 경우에는 불투명 한 명명 된 구조체 대신 불투명 한 핸들이 사용되는 이유가 있습니다.


답변

핸들 접근 방식의 가장 확실한 이점은 외부 API를 중단하지 않고 내부 구조를 수정할 수 있다는 것입니다. 물론 클라이언트 소프트웨어를 수정해야하지만 최소한 인터페이스는 변경하지 않아도됩니다.

다른 하나는 런타임마다 각기 다른 API 인터페이스를 제공 할 필요없이 여러 가지 가능한 유형 중에서 선택할 수있는 기능을 제공하는 것입니다. 각 센서가 약간 다르고 약간 다른 데이터를 생성하는 여러 가지 센서 유형의 센서 판독 값과 같은 일부 애플리케이션은이 접근 방식에 잘 반응합니다.

어쨌든 클라이언트에 구조를 제공 할 것이기 때문에 캐스팅이 필요한 API 임에도 불구하고 훨씬 더 간단한 API를 위해 약간의 유형 안전성 (런타임에서 여전히 확인할 수 있음)을 희생해야합니다.


답변

데자뷰

명명 된 불투명 구조체보다 불투명 핸들이 더 나은 방법은 무엇입니까?

나는 정확히 동일한 시나리오를 만났지만 약간의 미묘한 차이가 있습니다. SDK에는 다음과 같은 많은 것들이있었습니다.

typedef void* SomeHandle;

나의 단순한 제안은 그것이 우리의 내부 유형과 일치하게하는 것이었다.

typedef struct SomeVertex* SomeHandle;

SDK를 사용하는 타사에게는 아무런 차이가 없습니다. 불투명 한 타입입니다. 누가 신경 쓰나요? ABI * 또는 소스 호환성에는 영향을 미치지 않으며 새로운 SDK 릴리스를 사용하려면 플러그인을 다시 컴파일해야합니다.

gnasher가 지적했듯이 struct와 void와 같은 포인터의 크기가 실제로 다른 크기 일 수있는 경우가있을 수 있습니다.이 경우 ABI에 영향을 미칩니다. 그와 같이, 나는 실제로 그것을 경험 한 적이 없다. 그러나 그 관점에서 두 번째는 실제로 모호한 맥락에서 이식성을 향상시킬 수 있기 때문에 대부분의 사람들에게 무례한 것이지만 두 번째를 선호하는 또 다른 이유입니다.

타사 버그

또한 내부 개발 / 디버깅의 유형 안전성보다 더 많은 원인이있었습니다. 우리는 이미 두 개의 유사한 핸들 ( PanelPanelNew) void*이 핸들에 대해 typedef를 사용 했기 때문에 코드에 버그가있는 많은 플러그인 개발자를 가지고 있었고 실수로 잘못된 핸들을 잘못된 결과로 전달했기 때문에 실수로 사용했습니다. void*모두를위한. 그래서 실제로 사용 하는 사람들에게 버그를 일으켰습니다SDK. 그들의 버그는 또한 SDK의 버그에 대해 불평하는 버그 보고서를 보내므로 내부 개발 팀이 막대한 시간을 소비했습니다. 플러그인을 디버깅하고 실제로 플러그인의 버그로 인해 잘못된 핸들을 전달했음을 발견해야했습니다. 잘못된 위치로 (모든 핸들이 void*또는 의 별칭 인 경우 경고없이 쉽게 허용됨 size_t). 우리가 불필요하기 때문에 모든 내부 정보, 심지어 단순한 멀리 숨어있는 개념 순도에 대한 우리의 욕망에 의해 그들의 부분에 발생하는 실수 제 3 자에 대한 디버깅 서비스를 제공하는 우리의 시간을 낭비했다 그래서 이름 내부의를 structs.

Typedef 유지

차이가 나는 우리가에 충실 할 것을 제안하고 있다는 것입니다 typedef여전히 가지고 있지 고객 쓰기 것입니다 미래의 플러그인 버전의 소스 호환성에 영향을 미칩니다. SDK 관점에서 C로 typedefing하지 않는 아이디어를 개인적으로 좋아하지만 요점은 불투명하기 때문에 도움 이 될 수 있습니다. 공개적으로 공개 된 API에 대해서만이 표준을 완화하는 것이 좋습니다. SDK를 사용하는 클라이언트의 경우 핸들이 구조체, 정수 등을 가리키는 포인터인지 여부는 중요하지 않습니다. 중요한 것은 핸들이 서로 다른 두 핸들이 동일한 데이터 유형의 별칭을 지정하지 않기 때문입니다. 잘못된 핸들을 잘못된 장소로 잘못 전달하십시오.struct SomeVertexstructtypedef

타입 정보

캐스팅을 피하는 것이 가장 중요한 곳은 내부 개발자입니다. SDK에서 모든 내부 이름 을 숨기는 이러한 종류의 미학은 모든 유형 정보를 잃어 버리는 데 상당한 비용을들이는 개념 미학이며, 중요한 정보를 얻기 위해 캐스트를 디버거에 불필요하게 뿌려야합니다. C 프로그래머는 C에서 이것에 익숙해야하지만, 불필요하게 요구하는 것은 문제를 요구하는 것입니다.

개념적 이상

일반적으로 모든 실용적이고 일상적인 요구보다 순도에 대한 개념적인 아이디어를 제공하는 개발자 유형에주의하십시오. 이것들은 유토피아 적 이상을 추구 할 때 코드베이스의 유지 보수성을 땅에 가져다 줄 것입니다. 팀 전체가 부자연스럽고 비타민 D 결핍을 유발할 수 있다는 두려움 때문에 사막에서 선탠 로션을 피할 수 있습니다.

사용자 엔드 환경 설정

API를 사용하는 사람들의 엄격한 사용자 엔드 관점 에서조차도 버그가 많은 API 또는 잘 작동하지만 교환 할 때 거의 신경 쓰지 않는 이름을 노출시키는 API를 선호합니까? 그것이 실질적인 절충이기 때문입니다. 일반적인 맥락에서 불필요하게 유형 정보를 잃어 버리면 버그의 위험이 높아지고, 수년에 걸쳐 팀 전체의 대규모 코드베이스에서 머피의 법칙은 상당히 적용 가능한 경향이 있습니다. 버그의 위험을 불필요하게 늘리면 적어도 더 많은 버그가 생길 수 있습니다. 상상할 수있는 모든 종류의 인간의 실수가 결국 잠재력에서 현실로 갈 것이라는 것을 발견하기 위해 대규모 팀 환경에서 너무 오래 걸리지 않습니다.

아마도 그것은 사용자에게 제기되는 질문 일 것입니다. “버그가 많은 SDK 또는 당신이 신경 쓰지 않을 내부 불투명 이름을 노출시키는 SDK를 선호하겠습니까?” 그리고 그 질문이 잘못된 이분법을 제시하는 것 같다면, 버그에 대한 높은 위험이 궁극적으로 장기적으로 실제 버그를 나타낼 것이라는 사실을 이해하기 위해서는 대규모 환경에서 더 많은 팀 차원의 경험이 필요하다고 말합니다. 버그를 피하는 데 개발자가 얼마나 확신하는지는 중요하지 않습니다. 팀 전체 환경에서 가장 취약한 링크와 트립되지 않도록하는 가장 쉽고 빠른 방법에 대해 더 많이 생각할 수 있습니다.

신청

따라서 여기에서 여전히 모든 디버깅 이점을 유지할 수있는 타협을 제안합니다.

typedef struct engine* enh;

… typedefing의 비용에도 불구하고, struct그것은 정말로 우리를 죽일 것인가? 아마 그렇지는 않을 것입니다. 따라서 필자는 부분적으로 실용주의를 권장하지만, size_t99를 사용하는 추가 정보를 숨기는 것을 제외하고는 여기 를 사용 하고 정수로 캐스팅하거나 정수로 캐스팅 하여 디버깅을 더 어렵게 만드는 개발자에게 더 좋습니다. %는 사용자에게 숨겨져 있으며 더 이상 해를 끼칠 수 없습니다 size_t.


답변

나는 진짜 이유가 관성이라고 생각합니다. 그것이 그들이 항상 한 일이며 그것이 왜 바뀌는가?

내가 볼 수있는 주된 이유는 불투명 핸들이 디자이너가 구조체뿐만 아니라 그 뒤에 모든 것을 넣을 수 있기 때문입니다. API가 여러 개의 불투명 한 유형을 반환하고 수락하면 모두 호출자와 동일하게 보이며 미세한 인쇄가 변경되는 경우 컴파일 문제 나 다시 컴파일이 필요하지 않습니다. en_NewFlidgetTwiddler (handle ** newTwiddler)가 핸들 대신 Twiddler에 대한 포인터를 리턴하도록 변경되면 API는 변경되지 않으며 새 코드는 자동으로 핸들을 사용하기 전에 포인터를 사용합니다. 또한, OS를 넘어 서면 포인터를 조용히 고정시키는 OS 나 그 밖의 어떤 위험도 없습니다.

물론 단점은 호출자가 그 안에 아무것도 넣을 수 없다는 것입니다. 64 비트가 있습니까? API 호출에서 64 비트 슬롯에 넣고 어떻게되는지 확인하십시오.

en_TwiddleFlidget(engine, twiddler, flidget)
en_TwiddleFlidget(engine, flidget, twiddler)

둘 다 컴파일하지만 그중 하나만 원하는 것을합니다.


답변

저는 C 라이브러리 API를 초보자가 남용하지 못하도록 방어한다는 오랜 철학에서 비롯된 태도라고 생각합니다.

특히,

  • 라이브러리 작성자는 그것이 구조체에 대한 포인터라는 것을 알고 있으며 구조체의 세부 사항은 라이브러리 코드에 표시됩니다.
  • 라이브러리 를 사용 하는 모든 숙련 된 프로그래머 도이 라이브러리가 불투명 한 구조체에 대한 포인터임을 알고 있습니다.
    • 그들은 그 구조체에 저장된 바이트를 엉망으로 만들지 않을 정도로 충분히 고통스러운 경험을했습니다 .
  • 경험이없는 프로그래머는 어느 것도 모른다.
    • 그들은 memcpy불투명 한 데이터를 시도 하거나 구조체 내부의 바이트 또는 단어를 증가시킵니다. 해킹 해

오랜 전통적 대책은 다음과 같습니다.

  • 불투명 핸들은 실제로 동일한 프로세스 메모리 공간에 존재하는 불투명 구조체에 대한 포인터라는 사실을 가장합니다.
    • 그렇게하려면 비트와 같은 수의 비트를 갖는 정수 값이라고 주장하면됩니다 void*
    • 불필요한주의를 기울이려면 포인터의 비트도 가장하십시오 (예 :
      struct engine* peng = (struct engine*)((size_t)enh ^ enh_magic_number);

이것은 단지 오랜 전통을 가지고 있다는 것입니다. 나는 그것이 옳고 그른지에 대한 개인적인 의견이 없었습니다.