이 영상은 Let's Get Rusty의 후원을 받았습니다 . 오늘은
시스템 수준에서 작업하지 않는 한 생각해 볼 일이 거의 없는 몇 가지 저수준 개념에 대해 다뤄보겠습니다. 제가 가장 자주 받는 영상 요청 중 하나는
일부 프로젝트에서 여러 프로그래밍 언어가
개발에 사용되는 이유를 설명해 달라는 것입니다. 이를 설명하는 것은 프로젝트 유형에 따라 매우 쉬울 수도 있고 매우 어려울 수도 있습니다. 예를 들어, Django와 같은 풀 스택 프레임워크를 생각해 보세요
. 파이썬은 서버에서 실행되는 백엔드를 처리하는 데 사용되고,
HTML, CSS 및 자바스크립트는 클라이언트 측에 표시되는 사용자 인터페이스를 구축하는 데 사용됩니다.
이 프로젝트는 다국어 프로젝트입니다. 하지만 이 경우에는 실제
운영 환경에서 모든 것이 어떻게 작동하는지 이해하기 쉽습니다. 왜냐하면 우리는 기본적으로 프로세스 간 통신을
사용하여 런타임에 원격으로 통신하는 두 개의 별개의 프로세스를 개발하고 있기 때문입니다 .
하지만 서로 다른 프로그래밍 언어로 작성된 구성 요소들이
단일 프로세스로 함께 실행되도록 설계된 프로젝트 유형도 있습니다. 그렇다면 이런 프로젝트들은 어떻게 가능한 걸까요?
안녕하세요 여러분, 제 이름은 조지이고 이곳은 코어 덤프트입니다.
이 채널을 구독하시는 분이라면, 저수준 시스템에 관심이 많으실 거라고 확신합니다. 그래서
이번 영상이 Let's Get Rusty의 후원을 받았다는 사실을 알려드리게 되어 정말 기쁩니다. 솔직히 말해서, Rust는
더 이상 미래의 언어가 아닙니다. 그것은 현대의 언어입니다.
제 말을 그대로 믿지 마세요. 업계를 한번 살펴보세요. 대기업들이
핵심 시스템 구축을 위해 Rust에 막대한 투자를 하고 있습니다. 구글, 마이크로소프트, 심지어 리눅스 커널
자체도 이제 러스트를 핵심 시스템에 통합하고 있습니다. 그건 과장이 아니에요.
그 일이 벌어지고 있어요. 개인적인 성장을 위해서든,
실제 시스템 개발 관련 직업을 얻기 위해서든, Rust 실력을 향상시키고 싶다면 Let's Get Rusty가 바로 최적의 Rust
교육 기관입니다. 같은 유튜버이자
러스트 온라인 커뮤니티에서 가장 사랑받는 인물 중 한 명이 만든 Let's Get Rusty는
저를 포함한 수천 명의 개발자들이 러스트 언어를 마스터하고 시스템
프로그래밍 분야에 진출하는 데 도움을 주었습니다. 그들은 곧 새로운 기수 학생들을 모집할 예정입니다. 자리가
한정되어 있으니 지금 확인해 보는 게 좋을 거예요 . let's getrusty.com/startwithjorge를 방문하시거나
아래 고정 댓글의 링크를 클릭하세요. 채널을 후원해주신 let's getrusty 님께 진심으로 감사드립니다 . 자
, 그럼 오늘의 영상으로 넘어가 볼까요? 이해를 쉽게 하기 위해, 우선
기계어로 컴파일되는 프로그래밍 언어만 고려해 보겠습니다. 일반적으로 각 프로그래밍 언어에는 자체적인
컴파일러가 있습니다. 그러니까, 예를 들어 Rust 파일을 가져와서
Go 컴파일러로 컴파일할 수는 없습니다. 바로 그 지점에서 상황이
흥미로워지면서도 약간 혼란스러워집니다. 대부분의 프로그래밍 언어가 별도의
컴파일러, 런타임, 메모리 모델을 사용한다면, 어떻게 이 모든 것이
하나의 바이너리 파일 안에 공존할 수 있을까요? 사람들이 흔히 혼동하는 것은
컴파일러가 소스 코드를
실행 파일로 직접 변환하는 도구에 불과하다는 지나친 단순화입니다. 오해하지 마세요. 컴파일러가
실행 파일을 생성하는 것은 맞지만, 그것은 우리가 일반적으로 볼 수 없는 훨씬 더 복잡한 여러 단계 과정의 최종 결과물일 뿐입니다
. 이를 설명하기 위해
간단한 C 프로그램을 살펴보겠습니다. C 개발자가 아니더라도 걱정하지 마세요. 이 프로그램은
단순히 메시지를 출력하지만, 출력되는 메시지는 실행되는 운영 체제에 따라 달라집니다.
대부분의 GNU Linux 시스템에서 C 언어용 기본 컴파일러는 GCC입니다. 예전에는
GNU C 컴파일러를 의미했지만, 더 이상 그렇지 않습니다 . 잠시 후, 그
이유를 알게 되실 겁니다. C 프로그램을 컴파일하고 실행하려면
일반적으로 GCC를 호출하고 컴파일하려는 파일들을 전달하기만 하면 됩니다. 그러면
실행 파일이 생성됩니다. 우리 입장에서는 단 두 가지
간단한 단계일 뿐입니다. 하나는 프로그램을 컴파일하는 데 쓰고, 다른 하나는 실행하는 데 씁니다. 하지만 내부적으로
컴파일러는 훨씬 더 많은 일을 하고 있습니다. GCC는 내부적으로
C 파일을 실행 가능한 파일로 변환하기 위해 네 가지 주요 단계를 거칩니다 . GCC는 그
자체로 심층 분석이 필요한 주제이지만, 여기서는 간단하게 설명하겠습니다.
첫 번째 단계는 전처리입니다. 이 과정은 주석 제거,
매크로 확장, 조건부 컴파일 해결, 그리고 특히
포함 파일 해결과 같은 작업을 통해 소스 코드를 준비합니다. include를 사용하면 C 전처리기가 해당 줄을
헤더 파일과 해당 헤더 파일이 포함하는 모든 헤더 파일의 내용으로 대체하여 컴파일이 시작되기 전에 해당 코드를 파일에 삽입합니다. 따라서 출력 결과는
여전히 C 코드이지만, 다음 단계를 위해 사전 처리된 형태입니다 .
다음 단계는 컴파일이지만, 기계어로 직접 컴파일되는 것은 아닙니다 . 대신,
사전 처리된 코드는 어셈블리 언어로 변환되는데 , 이는
컴퓨터가 실행할 명령어 이지만 여전히 사람이 읽을 수 있는
언어로 되어 있습니다. 자 , 그럼 첫 번째 잘못된 상식을 바로잡아 보겠습니다.
컴파일러가 항상 소스 코드를 기계어로 변환하는 것은 아닙니다. 실제로 많은
컴파일러는 소스 코드를 어셈블리와 같은 중간 표현
이나 다른 프로그래밍 언어로 변환합니다.
세 번째 단계는 어셈블러를 사용하는 단계입니다. 어셈블러는 엄밀히 말하면 또 다른 컴파일러이지만,
이전 단계에서 생성된 사람이 읽을 수 있는 어셈블리 코드를 CPU가 이해하는 기계어 코드, 즉 1과 0으로 변환합니다. 그
결과물을 오브젝트 파일이라고 합니다. 하지만 여기에 함정이 있습니다. 이 오브젝트 파일은
아직 실행할 수 없습니다. GCC는 함수가 바이너리 내에서 어디에
배치될지 아직 결정해야 합니다. 이 간단한 예시에서는
콘솔에 텍스트를 출력하는 것만으로 충분합니다. 하지만 printf 함수의 실제 구현은 C
표준 라이브러리에 있다는 점을 기억하세요. 따라서 해당 라이브러리도 방금 설명한 것과 동일한 컴파일 단계를 거쳐야 합니다.
이제 마지막 단계인 연결 단계로 넘어갑니다. 이 단계에서는 코드
에서 생성된 오브젝트 파일과 개발 중에 포함시킨 외부 라이브러리에서 생성된 오브젝트 파일 등 여러 개의 오브젝트 파일이 있을 수 있습니다 .
링커의 역할은 이러한 모든 오브젝트 파일을 하나의
독립적인 실행 파일로 결합하는 것입니다. 이 일을 하는 데는 두 가지 방법이 있습니다. 가장 쉬운 방법은 라이브러리에서
필요한 각 함수의 기계어 코드를 가져와 최종 실행 파일에 복사하는 것입니다.
이를 정적 링크라고 합니다. 우리 프로그램에 필요한 모든 라이브러리 함수는
출력 파일에 직접 포함되어 있습니다. 모든 것이 자체적으로 완비되어 있어
원할 때 언제든지 실행할 수 있습니다. 하지만 또 다른 옵션은 동적 링크입니다.
시스템에서 표준 라이브러리의 print 함수를 사용하는 프로그램이 몇 개나 되는지 생각해 보세요 . 만약 모든
프로그램이 해당 함수의 정적인 복사본을 포함시킨다면,
디스크 전체에 수천 개의 동일한 복사본이 저장될 것입니다. 동적 링크를 사용하면 라이브러리가
동적 공유 라이브러리라는 특수한 유형의 파일로 미리 컴파일됩니다. 유닉스 계열 시스템에서 이러한 라이브러리는
SO 파일 확장자를 갖습니다. Windows 환경에서는 DLL 확장자로 식별됩니다
. 이러한 동적 공유 라이브러리는
라이브러리가 제공하는 함수에 대한 실행 코드를 포함하고 있다는 점에서 실행 파일과 유사합니다. 가장 큰 차이점은 실행을
시작하는 진입점이 없다는 점인데 , 이는
라이브러리에는 일반적으로 프로그램을 시작하는 데 사용되는 main 함수가 없기 때문에 당연한 것입니다
. 프로그램을 동적 링크로 컴파일하면 링커는
라이브러리의 함수를 실행 파일에 직접 복사하지 않습니다. 대신,
해당 함수의 기계어 명령어가 포함된 라이브러리에 대한 참조를 삽입할 뿐입니다. 실행 시 프로그램이
해당 동적 라이브러리의 함수를 필요로 하는 경우, 운영 체제는 필요한
함수를 프로그램의 주소 공간에 로드하여 프로그램이 마치
실행 파일의 일부인 것처럼 사용할 수 있도록 합니다. 처음에는 다소 이상하게 들릴지 모르지만, 실제로는 매우
효율적입니다. 이 시스템은 서로 다른 프로그램에서 동일한 함수의 복사본을 여러 개 저장하는 대신
라이브러리를 한 번만 저장합니다. 해당 라이브러리가 필요한 각 프로그램은 단순히
공유 라이브러리를 참조하고, 필요할 때 런타임에 요청 시에만 로드합니다. 이렇게 하면
디스크 공간과 메모리가 모두 절약됩니다. 특히
공통 라이브러리에 의존하는 프로그램이 많은 시스템에서 매우 큰 장점입니다 . 또한 라이브러리를 사용하는 모든 프로그램을 다시 컴파일할 필요 없이
라이브러리를 업데이트하거나 패치할 수 있으므로 더 유연합니다 .
정적 링크와 동적 링크 모두 심오한 주제이기 때문에 솔직히 말해서 별도의 영상으로 다룰 가치가 있습니다.
더 자세히 알고 싶으시다면 댓글로 알려주세요.
링크 작동 방식, 특히 동적 라이브러리의 모든 것을 자세히 설명하는 에피소드를 따로 제작하겠습니다.
자, 이제 컴파일 단계로 돌아가 보겠습니다. 모듈화가 도대체 무슨 의미가 있는지 궁금하실 수도 있습니다.
컴파일러가 소스 코드에서 실행 파일로 바로 변환할 수 있다면 왜 과정을 여러 단계로 나누는 걸까요?
일반적으로 이러한 중간 단계를 모두 볼 수 없는 이유는
GCC와 같은 컴파일러가 기본적으로 이러한 단계를 숨기도록 설정되어 있기 때문입니다. 그들은
최종 결과물인 실행 파일만 보여줍니다. 하지만 적절한 플래그를 사용하면 이러한 모든 단계를 드러낼 수 있습니다
. 예를 들어 GCC를 사용하여
프로그램을 컴파일할 때 임시 저장 플래그를 추가하면 최종
실행 파일뿐만 아니라 모든 중간 파일도 함께 생성됩니다.
특정 단계에서 프로세스를 중단할 수도 있습니다. 예를 들어, S 플래그는 어셈블리 코드를 생성한 후 프로세스를 중지합니다.
이는 고급 C 코드가 어셈블리 코드나 기계어로 어떻게 변환되는지 확인하고 싶을 때 교육 환경에서 매우 유용합니다
. 전문적인 환경에서는 성능에 중요한 영향을 미치는 코드를 검사하는 데에도 사용됩니다.
컴파일러가 효율적인 명령어를 생성하는지 확인하려면 생성된 어셈블리 코드를 살펴보면 됩니다.
더욱 흥미로운 점은 파이프라인의 어느 단계에서든 시작할 수 있다는 것입니다.
GCC에 어셈블리 파일을 전달하고 어셈블 및 링크를 수행하도록 지시하기만 하면 됩니다.
이는 엄청난 발전입니다. 왜냐하면 코드의 일부를 어셈블리로 작성하고,
컴파일러에 여러 단계에서 전달한 다음, 링커가
이들을 결합하여 실행 파일을 만들어 주기 때문입니다.
이것만으로도 우리의 원래 질문에 대한 답이 나오기 시작합니다 .
예시를 통해 살펴보겠습니다. 주어진 숫자와 0 사이에 존재하는 소수의 개수를 계산하는 프로그램을 작성해야 한다고 가정해 봅시다 . 그리고 우리는 그것이 가능한 한 빨리 이루어지기를 원합니다
. 우리는 전체 코드를 C 언어로 작성할 수도 있습니다. 하지만
컴파일러 최적화를 신뢰하지 않는다고 가정해 봅시다. 그래서 우리는 복잡한 계산 함수를
어셈블리로 직접 작성하고 C 코드에서 호출하기로 결정했습니다.
그런 다음 두 파일을 GCC 컴파일러에 전달하면 C 코드가 컴파일 및 어셈블되고,
어셈블리 코드가 어셈블되고, 두 오브젝트 파일이 하나의 실행
파일로 링크됩니다. 자, 이렇게 해서
다국어 프로젝트를 완성했습니다. 이 기술은
리눅스 커널, ffmpeg, OpenSSL 및 다양한 임베디드 프로젝트와 같은 실제 시스템에서 사용됩니다. 이러한 프로그램
들은 대부분의 로직에 C 언어를 사용하지만 ,
성능이 정말 중요한 경우에는 어셈블리 언어로 전환합니다. 자, 여러분 중 많은 분들이 이미
짐작하셨을지도 모르지만, 그래도 다시 한번 말씀드리겠습니다. 우리가
흔히 C 컴파일러라고 부르는 GCC 같은 것은 단 하나의 컴파일러가 아닙니다. 실제로는 일련의
도구들이 순차적으로 실행되는 도구 체인, 즉 파이프라인입니다 . 각 단계는
이전 단계의 출력을 소비합니다. 그리고 이러한 도구들은 각각 플러그인 방식으로 사용할 수 있습니다.
공구 체인의 일부를 교체하거나 다양한 지점에 자체 파일을 삽입할 수 있습니다.
이것이 바로 GCC가 C 언어만 지원하는 것이 아니라, 설정 방식에 따라 C++, Objective C, Forran, ADA, D, 심지어 Go까지 지원하는 이유입니다. 원래 GCC는
GNU C 컴파일러의 약자였지만, 시간이 지나면서 C 언어 외에도 다양한 프로그래밍 언어를 지원하는 컴파일러 제품군으로 발전했습니다. 이러한 확장으로 인해
GNU C 컴파일러라는 이름은 오해를 불러일으키게 되었습니다. 그래서 GCC라는 약어는
GNU 컴파일러 모음을 의미하는 것으로 재정의되었습니다. 저는 이 점을 이해하는 것이 정말 중요하다고 생각합니다
. 누군가가 무심코 GNU C 컴파일러라고 부를 때마다,
이 시스템 전체가 C++ 코드를 실행 가능한 파일로 변환하는 하나의 블랙박스라는 인식을 의도치 않게 강화할 수 있습니다
. 하지만 사실은, 지난 몇 년 동안 상황이 전혀 그렇지 않았습니다. 알겠습니다만
, 조립은 아무나 할 수 있는 일은 아니죠. 솔직히 말해서 어셈블리 언어는
이미 컴파일 파이프라인의 일부이기 때문에, 이를 사용하는 것은 일종의 반칙처럼 느껴집니다
. 그렇다면 고어들을 섞어 쓰는 건 어떨까요 ? 예를 들어,
어셈블리 언어로 함수를 작성하는 대신 프로젝트의 일부를 포란으로 구현하면 어떨까요
? 네 , 충분히 가능한 일이고 생각보다
훨씬 흔한 일입니다. 하지만 이 경우에는 대개
여러 단계를 거쳐야 합니다. 하나는 Forran 파일을 컴파일하고 어셈블하는 작업, 다른 하나는
C 파일을 컴파일하고 어셈블하는 작업, 그리고 세 번째는 두 오브젝트 파일을
하나의 실행 파일로 어셈블하는 작업입니다. C 컴파일 파이프라인에 이미 내장된 어셈블리 언어와 달리,
포란 언어는 자체 파이프라인, 자체 컴파일러를 가지고 있으며, 경우에 따라서는 자체
런타임 종속성까지 갖추고 있습니다. 이제 우리의 원래 질문, 즉 어떻게 서로 다른
언어가 하나의 실행 파일 안에 공존할 수 있는지에 대한 답은 링커에 있다는 것이 아주 명확해졌을 것입니다. 보시다시피
, 관련된 여러 언어들은 GCC와 같은 컴파일러 제품군에서 나올 필요조차 없습니다. Rust를 예로 들어보겠습니다
. C 언어와는 완전히 다른 툴체인을 가지고 있습니다. 컴파일러도 다르고,
빌드 시스템도 다르고, 철학 자체도 완전히 다릅니다. 저는 이 제품의
컴파일러에 담긴 놀라운 엔지니어링 기술에 대해 몇 시간이고 이야기할 수 있습니다. 하지만 우리가 중요하게 생각하는 건,
최종 바이너리 파일을 생성할 때 어떤 결과가 나올지입니다. Rust 2는 링커에 의존합니다.
C 언어에서 Rust 함수를 호출하려면 다음과 같이 하면 됩니다. 우리는 해당 기능을 Rust로 구현합니다. 우리는
Rust 코드를 정적 라이브러리 또는 동적 라이브러리로 컴파일합니다 . 우리는
C 코드에서 함수를 선언하고 사용합니다. 다음으로 C 코드를 컴파일하고 Rust로
컴파일된 라이브러리와 링크합니다. 물론, 그 반대의 경우도 마찬가지입니다
. Rust에서 C 함수를 호출할 수 있습니다. 모든 것은 우리가
달성하고자 하는 목표에 달려 있습니다. 사실, Rust에서 C 코드를 호출하는 것이 그 반대의 경우보다 더 흔합니다
. C 언어는 역사가 오래되었고, 많은 성숙한 라이브러리와 시스템 API가 C로 작성되었습니다.
Rust 개발자는 특히
그래픽, 암호화 또는 운영 체제 API와 같은 분야에서 기존 생태계를 활용해야 하는 경우가 많습니다. 하나의 프로젝트
에서 여러 프로그래밍 언어를 혼합하여 사용하려는 데에는 여러 가지 이유가 있을 수 있습니다 . 하지만 또 하나
떠오르는 것은 성능입니다. 많은 프로젝트에서 시스템 전체가 엄청나게 빠를
필요는 없고, 특정 부분만 빠르면 되는 경우가 많습니다. 그래서 많은
개발자들이 편의성과 개발 속도를 위해 프로젝트의 대부분을 고급 언어로 작성하고,
성능이 중요한 부분만
C와 같은 저급 언어로 구현합니다. 마무리하기 전에
이해해야 할 정말 중요한 점이 하나 더 있습니다. 두 개의 고지능 언어, 즉
언어 A와 언어 B가 있다고 가정해 봅시다. 두 언어 모두 최종 링크 단계를 거친다고 해서
자동으로 하나의
실행 파일로 올바르게 링크될 수 있는 것은 아닙니다. 아주 간단한 예를 들어보겠습니다. 우리는 언어 B로 함수를 구현했고,
언어 A에서 그 함수를 호출하고 있습니다. 두 컴파일러가 동일한 아키텍처에 대한 어셈블리 코드를 생성하더라도,
함수 간 데이터 전달 방식에 대해 서로 다른 가정을 할 수 있습니다. 예를 들어, 언어 A용 컴파일러는
두 함수 매개변수를 레지스터 0과 1에 전달할 수 있지만,
언어 B용 컴파일러는 매개변수가 레지스터 1과 2에 있을 것으로 예상할 수 있습니다.
둘 다 유효한 기계어 코드를 생성 하지만, 호출 규칙이
다르기 때문에 런타임에 정의되지 않은 동작이 발생합니다. 언어 A는
인수를 잘못된 위치에 배치하고, 언어 B는 잘못된 데이터를 사용하여 연산을 수행합니다
. 그리고 거기서 끝이 아닙니다. 이
예시에는 또 다른 문제가 있습니다. 함수는 결과를 계산한 후
레지스터 1에 기록하고 반환합니다. 하지만 언어 A는 결과가
레지스터 0에 있어야 한다고 예상합니다. 따라서 언어 B는 잘못된 결과를 계산할 뿐만 아니라,
언어 A는 그 결과를 전혀 인식하거나 사용하지도 않습니다 .
같은 예시이지만, 이번에는 두 언어 X와 Y를 사용합니다. 두 언어 모두
레지스터 0과 레지스터 1을 사용하여 매개변수를 전달하고 받습니다. 하지만
언어 X가 모든 함수 인수에 대해 참조에 의한 전달을 사용한다고 가정해 봅시다 .
변수의 주소를 레지스터에 저장하는 것이지, 변수의 값을
레지스터에 직접 저장하는 것과는 다릅니다. 한편, 언어 Y는 값으로 전달됩니다.
따라서 레지스터에 실제 값이 있을 것으로 예상합니다 . 그래서 여기서는
런타임에 호출되는 즉시 해당 레지스터에 내용을 추가합니다. 이러한 불일치로 인해 언어 Y는 메모리 주소를 실제 값으로 해석하여
해당 주소에 저장된 값 대신 해당 주소를 더하게 되므로 완전히 잘못된 동작이나
심지어 충돌이 발생할 수 있습니다. 따라서 두 컴파일러 모두
유효하고 실행 가능한 어셈블리 코드를 생성하더라도, 양측이 서로 통신하는 방식에 대해 합의하지 않으면 최종 링크된 바이너리는 일관성이 없게 됩니다
. 이러한 저수준 규칙은
애플리케이션 바이너리 인터페이스(ABI)라고 하는 것에 의해 정의됩니다. API가 애플리케이션 수준에서 기능을 정의하는 것처럼
, AI는 이진 코드의 다양한 구성 요소들이
하드웨어를 통해 서로 어떻게 상호 작용하는지를 정의합니다 .
따라서 서로 다른 두 언어를 혼합하여 사용할 때는 두 언어 모두 오브젝트 파일을 생성하는 것만으로는 충분하지 않습니다. 적어도 하나,
또는 특히 다른 언어와 상호 작용하는 부분은
다른 AI의 기대치를 충족해야 합니다 .
앞서 살펴본 언어 X와 언어 Y 예시에서, 이를 구현하는 한 가지 방법은
언어 Y를 수정하여 값을 역참조하도록 하는 것입니다 . 이렇게 하면 함수는 먼저
제공된 메모리 주소에서 데이터를 가져와 실제 값을 레지스터에 로드한 다음
덧셈을 수행합니다. 이 경우, 언어 Y는 인공지능이
언어 X에 기대하는 바에 맞추기 위해 만들어지고 있습니다. 하지만 정반대의 접근 방식을 취할 수도 있습니다
. 언어 X가 언어 Y의 AI 기대치에 부합하도록 하려면
인자 값을 주소 대신 레지스터에 직접 로드하기만 하면 됩니다 . 이렇게 하면
언어 Y의 함수는 호출될 때 즉시 값을 더할 수 있습니다 .
이 영상 초반에 언급했듯이, 이것들은 시스템 수준에서 작업하지 않는 한 일반적으로 생각할 필요가 없는 저수준 세부 사항입니다. 다행인 점은
언어 설계자들이 이 사실을 알고 있다는 것입니다. 최신 프로그래밍 언어는 이러한 과정을 더 쉽게 만들어주는 도구, 키워드 및 컴파일러 플래그를 제공합니다
. C 언어에서는 extern을 사용하여 외부 함수를 선언할 수 있습니다. Rust에서는 `
extern` 키워드와 `no mangle` 속성을 사용합니다.
포란에서는 bind 속성을 사용할 수 있습니다 .
Go 언어에서는 `import C` 줄 바로 위에 특수한 주석 블록을 배치하여
C 헤더 파일을 포함할 수 있습니다. 심지어 Go 소스 파일에 C 코드를 직접 인라인으로 작성할 수도 있습니다. 모든
언어는 이를 표현하는 고유한 방식을 가지고 있습니다. 하지만 컴파일 시점에는 이러한 선언들이
모두 동일한 목적을 수행합니다. 그들은 컴파일러에게 "이 함수는
다른 언어로 작성된 코드와 상호 작용할 것입니다 ."라고 알려줍니다. 생성된
어셈블리가 예상 ABI를 준수하는지 확인하십시오. 그럼 오늘은 여기까지 하도록 하겠습니다.
다음 파트에서는 컴파일 언어와 인터프리터
언어를 혼합하여 사용하는 방법을 다루겠습니다. 그러니 구독 버튼을 꼭 눌러주세요. 놓치고 싶지 않으실 거예요.
아래 고정 댓글에 링크된 Let's Get Rusty도 꼭 확인해 보세요. 이 영상이 마음에 들었거나
새로운 것을 배우셨다면 좋아요 버튼을 눌러주세요. 무료라서 저에게
큰 도움이 될 것 같아요. 다음 영상에서 만나요.