18P by xguru 20일전 | favorite | 댓글 12개

잘 된 것들

  • 재작성은 작은 단계로 이뤄졌고(점진적, stop-and-go), 잘 동작하며, 새 코드는 읽고 이해하기 쉬워졌음
  • 모든 코드를 보는 시야를 가짐으로써 성능 최적화 기회를 얻음
  • 1/3 ~ 1/2 정도의 사용하지 않는 코드를 제거함. 러스트나 Go 같은 현대 프로그래밍 언어는 데드 코드를 더 잘 찾아내고 개발자에게 알려줌
  • 범위 밖 접근이나 오버플로우/언더플로우 걱정이 없음
  • 내장된 테스트 프레임워크가 매우 유용함
  • CMake 파일을 제거할 수 있어 기쁨

잘 되지 않은 것들

여전히 정의되지 않은 행동을 추적해야 함

  • C/C++에서 러스트로 점진적 재작성을 하면서 많은 원시 포인터와 unsafe{} 블록을 써야 했음
  • 러스트 규칙이 unsafe 내에서도 적용되지만 컴파일러가 체크하지 않으므로 정의되지 않은 행동이 쉽게 발생함
  • unsafe 내에서는 여러 읽기 전용 포인터 XOR 한 개의 변경 가능 포인터 규칙을 쉽게 깰 수 있음
  • Miri가 이를 잡아주는 구원자 역할을 함

Miri가 항상 작동하지는 않고 여전히 Valgrind를 사용해야 함

  • 암호화 라이브러리처럼 C나 어셈블리로 작성된 부분이 있는 라이브러리를 사용하면 Miri가 작동하지 않음
  • Miri로 체크되지 않는 많은 unsafe 코드가 있음
  • 일부 테스트는 valgrind에서 실행해야 했음

여전히 메모리 누수를 추적해야 함

  • C API의 일반적인 패턴은 MYLIB_init()에서 메모리를 할당하고 MYLIB_release()에서 해제하는 것인데, MYLIB_release를 호출하는 걸 잊어 버리기 쉬움
  • 러스트 개발자는 RAII로 래퍼 객체를 만들고 싶어 하지만, C API를 사용하는 테스트에서는 이 기능을 쓸 수 없음
  • 복잡한 로직에서는 정리 함수를 항상 호출하기 어려움. C에서는 goto를 써서 해결하지만 러스트는 지원하지 않음
  • defer 크레이트로 해결했으나 빌림 체커가 좋아하지 않음

크로스 컴파일이 항상 작동하지는 않음

  • Miri와 마찬가지로 C나 어셈블리로 구현된 부분이 있는 라이브러리를 사용하면 cargo build --target=...이 바로 작동하지 않음

Cbindgen이 항상 작동하지는 않음

  • Cbindgen은 러스트 코드베이스에서 C 헤더를 생성하는 데 많이 쓰이지만 한계나 버그가 있음

불안정한 ABI

  • Option 같은 유용한 표준 라이브러리 타입에 안정된 ABI가 없어서 repr(C) 어노테이션으로 수동 복제해야 함

커스텀 메모리 할당자 지원 부재

  • 많은 C 라이브러리는 사용자가 런타임에 할당자를 제공할 수 있음. 러스트에서는 컴파일 타임에만 전역 할당자를 고를 수 있음
  • 자원 정리 이슈는 아레나 할당자로 해결할 수 있지만, 러스트에서는 관용적이지 않고 표준 라이브러리와 통합되지 않음

복잡성

  • UnsafeCell이나 RefCell, MaybeUninit, Pin 같은 걸 FFI 처리를 위해 써야 해서 복잡성이 높음
  • 순수 러스트도 이미 복잡한데 FFI 레이어까지 더해지면 야수가 됨
  • 러스트 복잡성 때문에 이 코드베이스 작업을 거절한 개발자들도 있었음

결론

  • 러스트 재작성에 대체로 만족하지만 일부 영역에서는 실망스러웠고, 예상보다 훨씬 더 많은 노력이 들었음
  • C와 많이 상호작용하는 러스트는 순수 러스트를 사용하는 것과는 완전히 다른 언어처럼 느껴짐. 마찰이 많고 함정도 많음. 러스트가 해결했다고 주장하는 C++의 많은 이슈가 사실 전혀 해결되지 않음
  • 러스트, Miri, cbindgen 등의 개발자들에게 깊이 감사함. 그들은 엄청난 일을 해냈음. 그럼에도 불구하고 C FFI를 많이 할 때의 언어와 도구는 미성숙하고 거의 v1.0 이전 같이 느껴짐
  • unsafe의 ergonomics, 표준 라이브러리, 문서, 도구, 불안정한 ABI 등이 향후 개선된다면 더 즐거운 경험이 될 수 있을 것임
  • 마이크로소프트와 구글도 이 모든 점을 느꼈기에 이 영역에 실제 자금을 투자하고 있는 것으로 보임
  • 아직 Rust를 모른다면 첫 프로젝트는 순수 Rust를 사용하고 FFI 주제와는 거리를 두는 것이 좋음
  • 처음에는 이 재작성에 Zig이나 Odin을 사용하는 것을 고려했지만 기업 프로덕션 코드베이스에 v1.0 이전 언어를 사용하고 싶지 않았음. 이제는 경험이 Rust보다 정말 나빴을지 궁금해짐. 아마도 Rust 모델이 C 모델(또는 C++ 모델)과 정말 맞지 않아서 둘을 함께 사용할 때 마찰이 너무 심한 것 같음
  • 앞으로 비슷한 작업을 해야 한다면 Zig을 강력히 고려할 것임. 누군가 "그냥 Rust로 다시 작성하세요"라고 말할 때마다 이 글을 보여주고 마음이 바뀌었는지 물어보시길

이 글 역시 Rust를 잘못 이해하고 달려들었네요.
내용을 보면 Rust 외부와 자주 통신해야하는 라이브러리 같은데, 그 시점에서 이미 더러워질수밖에 없죠... 애초에 네이티브 언어 치고 안 더러운게 없는데 러스트는 언어 차원에서 그걸 안전하게 감싼거기 때문에 언어 외부와 접점이 생길수록 이점이 많이 없어집니다.

러스트가 해결했다고 주장하는 C++의 많은 이슈가 사실 전혀 해결되지 않음

어느정돈 맞지만, 원문의 개발환경에선 해결이 안될 수 밖에 없는데 Rust를 만병통치약처럼 생각하고 접근한게 문제라고 봅니다.

그러게요 unsafe 떡칠했다고 당당히 써놓고 해결되지 않았다니...

명시적으로 unsafe한 부분이 소스코드에 표시되기 때문에 프로그램 진입점부터 unsafe 블럭을 쓰지 않는 이상 FFI의 영향범위가 다 식별돼서 유용할 것으로 기대했는데요. 필자분께는 별로 와닿지 않은 모양입니다

애초에 FFI를 사용한 시점부터 안전한 설계는 물 건너간 셈입니다.

저는 C/C++을 Rust로 점진적으로 변경하는 과정에서 unsafe를 쓸 수 밖에 없으므로 Rust로 변경하는게 의미가 없다. Rust로 점진적으로 바꾸기 보단 Zig를 택하겠다란 의미로 느껴졌는데 본문 어디에서 Rust 외부와 자주 통신해야하는 라이브러리라고 적혀있나요?

FFI를 쓴다는게 곧 Rust 외부와 통신한다는 뜻이죠.
그리고 본문 내용을 보면 단순하게 어떤 상태나 간단한 데이터를 주고받는 정도로 끝나는게 아니라 내외부가 복잡하게 상호작용하는 것으로 보입니다.

C로 작성된 라이브러리를 점진적으로 Rust로 바꾸려면 FFI는 어쩔 수 없지 않나요? 프로그램의 작은 부분들을 Rust로 바꾸고 나머지 C부분을 FFI로 처리해야 할 텐데 이런 작업들을 외부와 통신이라고 표현하신걸까요? 그렇다면 원 글쓴이분께서 Rust에 회의감이 드는건 자연스러울 수 있다고 생각합니다. 전체 코드를 한번에 바꾸지 않는 이상 Rust의 이점은 없으니 Zig를 추천하겠죠

저는 Rust가 C++이 가진 고질적인 문제를 해결할 수 없다고 생각하긴 합니다. 이는 문법적인 관점보다 실무적인 관점이죠.

그 이유론

  1. 이미 너무 많은 Production들이 C/C++을 쓰고있습니다. 그리고 안정적으로 잘 돌아가고 있죠. 그고 대부분은 굳이 이걸 Rust로 포팅하려 하지 않습니다.

  2. 애초에 하드웨어가 레퍼런스 카운트를 가정하고 만들어지지 않습니다. C/C++을 쓰는 많은 경우가 하드웨어, OS, 드라이버, 바이너리단을 고속으로 제어하기 위함인데 러스트를 지원하기 위해선 저수준 개발자는 결국 unsafe를 활용한 자원의 라이프사이클을 직접 관리해야하고 이 또한 큰 비용입니다.

저는 글쓴이분의 경험이 언어가 잠재한 가치와 이론적인 이야기보다 더욱 중요하다 생각합니다.
실제 C/C++수준의 언어가 필요로되는 분야의 자원관리 수준은 러스트로 대체하기엔 계륵과도 같다는 생각이 듭니다.

Zig가 pre v1이긴 해도 많은 c 라이브러리들을 쓸 수 있어서 생각보다 쓸만하긴 합니다. 돌아가고 있는 c 기반 프로젝트에 뭔가를 얹기에는 러스트보다는 지그가 나을 수 있습니다.

rust 살펴볼때, unsafe 란 keyword 를 보는 순간 쏴한 느낌이 들었고...