Lobsters

C for Rust Programmers

Rust 프로그래머를 위한 C

Rust를 먼저 배운 개발자가 C를 익히며 마주친 차이를 불리언, 문자열, 정수 폭, 오류 처리, 배열, 포인터 사례로 설명합니다. C의 동작을 이해하고 메모리·플랫폼 관련 실수를 피하는 데 도움이 되는 입문용 비교 글입니다.

AI 요약

Rust를 먼저 배운 개발자가 C를 공부하며 낯설게 느낀 언어의 특징을 정리합니다. C 입문서를 대신하는 튜토리얼은 아니며, Rust와 비교해 C 코드를 읽고 쓸 때 기억할 점을 사례로 설명합니다.

불리언과 문자열

C99 이전에는 불리언 기본형이 없어 정수 0과 1을 사용했습니다. C99는 선택적으로 포함하는 <stdbool.h> 헤더에 bool을 제공했고, true와 false도 각각 1과 0으로 정의했습니다. C23에서는 불리언이 언어 기본 기능으로 바뀌었지만, 이전 표준으로 컴파일한다면 헤더가 필요합니다. 글은 Clang의 C23 지원이 부분적일 수 있다고 덧붙입니다.

Rust의 &str은 주소와 길이를 함께 담아 보통 16바이트를 사용합니다. C 문자열은 별도의 길이 대신 끝에 널 문자(\0)를 붙입니다. 길이 변수를 따로 보관하지 않아도 되지만, 문자열마다 종료 문자 공간을 마련해야 하고 빈 문자열도 1바이트를 차지합니다. 중간에 널 문자가 들어가면 <string.h> 함수가 문자열을 잘못 처리할 수 있으며, 종료 문자를 빠뜨리면 범위를 벗어난 읽기가 생길 수 있습니다. 글의 C 문자열 뒤집기 예제는 strlen으로 길이를 구하고 malloc(len + 1)로 공간을 할당한 뒤, 마지막 위치에 널 문자를 직접 씁니다. Rust 예제와 달리 C에서는 할당 크기와 종료 문자를 개발자가 챙겨야 합니다. 다만 글의 각주에서는 Rust 예제가 관용적인 구현은 아니며 ASCII만 올바르게 처리한다고 설명합니다.

정수 폭과 오류 처리

C의 정수형 크기는 정확한 비트 수로 고정되지 않습니다. 예를 들어 long은 64비트 Unix에서 64비트지만 64비트 Windows에서는 32비트입니다. 플랫폼 간 일관성이 필요하다면 <stdint.h>의 uint8_t, int32_t 같은 고정 폭 정수형을 쓰라고 권합니다. 포인터 크기에 맞춘 크기에는 size_t와 ptrdiff_t를 제시하지만, Rust의 usize·isize와 의미가 완전히 같지는 않다고 주의를 덧붙입니다.

Rust의 Result는 오류 처리를 타입에 드러내지만, C 함수는 보통 -1이나 널 포인터 같은 값으로 실패를 알립니다. errno를 읽으면 오류 종류를 확인할 수 있지만, 호출자가 모든 실패 가능성을 기억해 검사해야 합니다. 예제의 malloc도 실패하면 NULL을 반환하므로 검사하지 않고 결과에 접근하면 세그멘테이션 오류가 날 수 있습니다. 글은 검사와 perror, exit를 사용한 처리를 보여주고, tagged union으로 Rust의 Result를 흉내 내는 방법도 소개합니다. 하지만 오류를 확인하기 전에 값에 접근하지 못하도록 막을 수는 없습니다. C는 안전한 추상화나 계약을 강제하는 기능이 적고 프로그래머에게 판단을 맡긴다는 점을 필자는 아쉽게 여깁니다.

필드, 배열, 포인터

C에서는 구조체 값의 필드에 점(.)을 쓰고, 구조체 포인터의 필드에는 화살표(->)를 씁니다. Rust는 두 경우 모두 점을 사용하므로 차이가 생깁니다.

함수에 배열을 전달하면 배열 자체가 아니라 첫 요소를 가리키는 포인터로 취급됩니다. 함수 안에서 매개변수에 sizeof를 적용하면 배열 전체가 아니라 포인터 크기를 얻습니다. 글의 예에서는 원소 세 개짜리 배열이 함수 밖에서 3바이트지만, 함수 매개변수에서는 64비트 환경의 포인터 크기인 8바이트로 출력됩니다. Clang은 함수 배열 매개변수에 sizeof를 적용할 때 경고를 내므로 이 실수를 찾는 데 도움이 됩니다.

C에는 Rust의 참조나 borrow checker가 없고, 포인터를 직접 다룹니다. 포인터 산술로 배열을 순회할 수도 있지만, 실수하면 버퍼 오버플로와 범위를 벗어난 쓰기가 발생해 보안 문제가 될 수 있다고 글은 경고합니다. 필자는 개인 프로젝트에서 C를 자주 선택할 것 같지는 않지만, 시스템 프로그래머에게 알아둘 가치가 있는 언어라고 말합니다.

Lobsters 반응

  • @veqq — Racket 컴퓨터과학 강의에 ‘어쩔 수 없이 C를 배워야 한다면’이라는 자료가 붙어 있는데, 저는 꽤 마음에 들었습니다.
  • @ssokolow — 이런 생각을 했어야 했는데 아쉽습니다. Python에서 Rust로 넘어간 뒤 MS-DOS 레트로 취미 프로젝트를 하려고 C를 배웠지만, 배운 내용을 글로 쓸 생각은 못 했습니다.
    • @chauhankiran — 이제 알았으니 한번 써보세요. 아니면 Rust와 비교하면서 struct나 union처럼 더 구체적인 주제를 다뤄도 좋겠습니다.
    • @ssokolow — 이미 초안으로 쌓아둔 글을 마칠 만큼 시간을 정리하면, 취미 프로젝트만의 주제를 다룰 수도 있겠습니다. 예를 들면 Rust나 Pascal처럼 길이를 함께 저장하는 문자열·슬라이스 타입을 직접 구현하고, 복사 없는 파싱을 하는 방법입니다. 생각보다 충분히 실용적입니다.
  • @viega — 모두 사실이지만, 주로 역사적 이유가 있습니다. C 코드에 의존하는 곳이 워낙 많아서 바꾸기 어렵습니다. 대부분의 경우 성능 좋은 가비지 컬렉터를 연결하기도 쉽고, FilC로 컴파일하면 Rust보다 나은 안전성을 얻을 수도 있습니다. C가 더 나은 언어라고 말하는 건 아닙니다. Rust는 약 40년 뒤에 만들어졌습니다. C보다 나은 언어를 만드는 일은 어렵지 않습니다. 그래도 Rust와 Zig가 C의 영역에서 경쟁하는 점은 높이 평가해야 합니다. 언젠가 C만큼 중요해질 수도 있습니다. 각주에 관해서는 한마디 덧붙이자면, Clang은 꽤 전부터 C23에서 <stdbool.h> 없이 true와 false를 받아들입니다.
    • @quad — 대부분의 경우 성능 좋은 가비지 컬렉터를 연결하기 쉽다고 하셨는데, 어떤 성능 좋은 가비지 컬렉터가 있나요? 10년 넘게 임베디드 C 말고는 써본 적이 없어서 요즘 애플리케이션용 C가 어떤지 궁금합니다.
  • @chrismorgan — 2014년에 ‘Rust 프로그래머를 위한 Python’이라는 짧은 책을 느슨하게 기획한 적이 있습니다. 유머를 담되 조금은 유익한 책을 생각했지만, 앞서 진행하던 프로젝트가 예상보다 오래 걸려 완성하지 못했습니다. 이런 종류의 책을 낼 시기는 지난 것 같기도 하지만, 훨씬 짧은 형태로 언젠가 해볼 생각은 있습니다. 문단 없이 산문도 많지 않은 손글씨 기술서라는 새 방향을 살펴보고 있으며, 50~100쪽을 넘기지 않을 것 같습니다.
  • @masklinn — 글에서 Rust로 쓴 문자열 뒤집기 함수가 관용적이지 않고 ASCII만 올바르게 처리한다는 지적에 덧붙입니다. 코드포인트를 기준으로 처리하므로 결합 문자가 없는 문자 체계, 필요한 문자가 모두 완성형으로 있고 입력도 정규화된 경우에만 해당 한 줄 코드가 올바르게 동작합니다. 아람어와 브라흐미계 문자에서 흔한 문제라고 생각하지만, 라틴 문자에서도 생길 수 있습니다. 예를 들어 과라니어의 g̃는 미리 조합된 문자가 없고 라틴 소문자 g와 결합 물결표의 조합으로만 존재한다고 알고 있습니다.

원문: bd103.dev / 번역·요약: Trawling