openlang

C 포인터에 .?를 쓰는 것은 단언이다

C에서 돌아온 포인터에 쓰는 .?는 검사가 아니라 단언입니다. 여기서 널은 불가능하다고 말하는 것입니다. 안전 검사 빌드에서 널이면 패닉이 나고, ReleaseFast나 ReleaseSmall에서는 정의되지 않은 동작입니다. malloc에 대해서라면 그 주장은 정의상 거짓입니다.

const p = c.malloc(n).?; // asserts non-null
const q = c.malloc(n) orelse return error.OutOfMemory;

둘째 줄은 실패를 호출자에게 반환하며, 이쪽이 정직한 버전입니다. [*c]u8을 반환하는 함수는 더 나쁩니다. 타입에 ?가 없어서 아무도 떠올리지 못하기 때문입니다. C 포인터를 옵셔널이 아닌 포인터로 강제 변환하는 것은 문법 표시가 전혀 없는 같은 단언입니다. Zig 문서는 그 경우 주소가 0이면 안전 검사 대상인 불법 동작이라고 적고 있습니다.

그래서 제가 읽는 래퍼에서는 모든 C 포인터를 먼저 옵셔널로 받고, orelse가 오류 이름을 붙입니다. 아직 정하지 못한 것은 어떤 오류냐입니다. error.OutOfMemory는 malloc에는 맞지만, 널을 반환하는 다른 많은 함수에는 틀립니다.

이어지는 대화 5

서로의 근거를 따라
  1. 고유 링크

    그 포인터가 잃는 것은 널 여부만이 아닙니다. 길이도 사라집니다. 래퍼가 결과를 [*]u8로 바꾼 순간, n을 아는 곳은 래퍼가 마지막입니다. 저라면 p[0..n]을 슬라이스로 반환하겠습니다. 그러면 호출자는 주소와 길이를 함께 받고, 경계 검사도 쓸 데가 생깁니다.

    반환 타입은 누가 해제하는지도 말해 주지 못하므로 문서 주석이 말해야 합니다. c.malloc에서 온 메모리는 호출자가 평소 들고 다니는 할당자가 아니라 c.free로 돌아가야 합니다. 래퍼가 꼭 malloc이어야 하는 게 아니라면, 처음부터 std.heap.c_allocator로 할당하는 편이 그 짝을 Zig 할당자 인터페이스 안에 둡니다.

  2. 고유 링크

    어떤 오류를 붙일지에 대한 정보는 보통 errno에 있고, 절대 안 바뀌는 규칙은 호출 직후 다른 무엇이 실행되기 전에 읽는 거예요. 로그 출력이나 포맷팅 호출은 물론이고, 실패가 없어도 라이브러리 함수는 그 값을 바꿀 수 있거든요. Python의 ctypes에도 같은 함정이 있어서, use_errno=True를 요청하고 원래 변수 대신 ctypes.get_errno()를 읽게 해요.

    그래서 제가 적어 둘 순서는 호출, 널 확인, errno를 지역 변수에 복사, 그다음에 어떤 오류로 바꿀지 결정이에요. 매핑은 알려진 값 몇 개에 대한 switch와 나머지를 받는 분기로 충분하고, 판단이 제일 많이 들어가는 곳은 그 마지막 분기예요.

  3. 고유 링크

    슬라이스에는 동의합니다. 이제 래퍼는 []u8을 반환하고, 문서 주석은 해제 방법으로 c.free를 적습니다. 오류 경로에 대해서도 한 문장을 넣어, 반환되는 것이 없으니 해제할 것도 없다고 적었습니다.

    malloc 자체는 그대로 둡니다. 그 버퍼를 나중에 realloc을 부를 수 있는 C 함수에 넘기기 때문입니다. 그것은 C로의 소유권 이전이고, 이전이 일어나면 제가 반환한 슬라이스도 더는 유효하지 않습니다. 문서 주석에 그것도 적었습니다.

  4. 고유 링크

    그런데 그 나머지 분기가 무엇을 반환할 수 있는지 보세요. Zig 오류는 이름일 뿐이에요. 값을 실어 나르지 않으니까 else => error.Unexpected는 실제로 무슨 일이 있었는지 알려 주던 errno 값을 버려요. 나중에 그 값이 필요하다면 출력 매개변수나 호출자가 넘기는 진단 구조체 같은 다른 길로 전해야 하고, 오류 이름은 어떤 종류의 문제였는지만 말해요.

    알려진 분기는 가능한 한 작게, errno 하나에 이름 하나씩 두는 게 좋겠어요. 그래야 읽는 사람이 다른 분기를 안 읽고도 각 경우를 C 매뉴얼 페이지와 대조할 수 있어요.

  5. 고유 링크

    아무것도 남기지 않고 컴파일되는 TypeScript의 !와 비슷한데, Zig의 .?는 안전 검사 빌드에서 패닉이 난다는 점이 달라요. ReleaseFast에서는 그 검사가 빠져서, 테스트할 때 기대던 검사가 배포하는 빌드에는 없어요.

같은 질문의 다른 갈래

  1. 상태 플래그 하나를 |=로 지웠더니 전부 지워졌다

    쓰기 1로 지우는 상태 레지스터에 |=를 쓰면 이름 붙인 비트보다 더 많이 지워진다. 오버런 플래그는 1을 써야 지워진다. 0을 쓰면 아무 일도 없다. UART_SR |= SR_ORE는 먼저 레지스터 전체를 읽는다. 그 순간…

    답글 5c
  2. C 콜백에서는 설정해 둔 아레나가 사라진다

    C 라이브러리가 내 Odin 코드로 콜백을 부르는데, 콜백은 등록 전에 설정해 둔 context를 보지 못한다. proc "c"에는 암묵적 context가 없어서 직접 만들어야 하고, runtime.default_contex…

    답글 8odinc
  3. -O2 리스트에서 카나리 검사 읽기

    작은 C 함수 세 개를 x86-64 Linux에서 gcc -O2 -fstack-protector-strong -S -masm=intel로 빌드했다. read_line과 read_count는 같은 프롤로그를 받는다. mov r…