C 언어에서 사용하기 쉬운 MLIR C API의 설계 원칙, 명명 및 소유권 규칙, 널 처리, 타입 계층, 출력, 공통 패턴과 API 확장 방법을 설명합니다.
현재 상태: 개발 중, API 불안정, 기본적으로 빌드됨.
많은 언어는 C와는 상호 운용할 수 있지만, 이름 맹글링과 메모리 모델의 차이 때문에 C++와는 더 어려움을 겪습니다. MLIR의 C API는 C에서 직접 사용할 수 있지만, 주로 상위 수준의 언어나 라이브러리 전용 구성으로 래핑하여 사용하도록 의도되었습니다. 따라서 API는 단순성과 최소 기능을 지향합니다.
참고: C API는 C++ API보다 더 안정적일 것으로 예상되지만, 현재는 안정성을 보장하지 않습니다.
이 API는 핵심 IR 구성 요소(속성, 블록, 연산, 리전, 타입, 값), 패스(Pass) 및 일부 기본적인 타입/속성 종류를 제공합니다. 핵심 IR API는 의도적으로 저수준입니다. 예를 들어, 연산의 피연산자와 속성에 대해 “의미적인” 이름을 부여하려 하지 않고 단순한 목록으로 노출합니다. 특정 방언의 사용자는 핵심 API를 방언 특화 방식으로 래핑하는 것이 기대되며, 예를 들어 ODS 백엔드를 구현할 수 있습니다.
핵심 IR 구성 요소는 C++에 존재하는 IR 객체에 대한 불투명한 핸들로 노출됩니다. 이들은 API 사용자가 내부를 조사하도록 의도되지 않았으며(많은 경우 의미 있게 조사할 수도 없습니다), 사용자는 적절한 조작 함수에 핸들을 전달하는 방식으로 사용해야 합니다.
핸들은 기반 객체에 대한 소유권을 가질 수도 있고, 가지지 않을 수도 있습니다.
모든 객체 이름에는 Mlir 접두사가 붙습니다. 이들은 typedef로 정의되어 있으며 struct 키워드 없이 사용해야 합니다.
모든 함수 이름에는 mlir 접두사가 붙습니다.
주로 MlirX 인스턴스에 대해 동작하는 함수는 mlirX 접두사를 사용합니다. 이들은(생성 함수는 예외) 대상 인스턴스를 첫 번째 인수로 받습니다. 예를 들어, mlirOperationGetNumOperands는 첫 번째 인수로 받는 MlirOperation을 검사합니다.
소유권 모델은 다음과 같이 명명 규칙에 인코딩되어 있습니다.
기본적으로 소유권은 이전되지 않습니다.
결과의 소유권을 호출자에게 이전하는 함수는 두 가지 형태 중 하나입니다.
mlirXCreate<...>라는 이름을 가지며, 예: mlirOperationCreate;mlirYTake<...>라는 이름을 가지며, 예: mlirOperationStateTakeRegion.일부 인수의 소유권을 가져가는 함수는 mlirY<...>OwnedX<...> 형태를 가지며, 여기서 X는 타입이나 그 인수에 대한 충분히 고유한 설명을 나타냅니다. 소유권은 피호출자에게 이전됩니다. 예: mlirRegionAppendOwnedBlock.
객체를 생성하는 함수는 기본적으로 그 소유권을 호출자에게 이전하지 않습니다. 즉, 인수로 전달된 다른 객체 중 하나가 소유권을 유지합니다. 이러한 함수는 mlirX<...>Get 형태를 가집니다. 예: mlirTypeParseGet.
호출자가 소유한 객체를 파괴하는 함수는 mlirXDestroy 형태입니다.
코드가 어떤 객체를 소유하면, 더 이상 필요하지 않을 때 그 객체를 파괴할 책임이 있습니다. 다른 객체들을 소유한 객체가 파괴되면, 그 객체들이 가리키던 핸들은 무효가 됩니다. 타입과 속성은 생성된 MlirContext가 소유한다는 점에 유의하십시오.
핸들은 널 객체를 가리킬 수 있습니다. 호출자는 mlirXIsNull(MlirX)를 사용해 객체가 널인지 확인할 책임이 있습니다. API 함수는 명시적으로 달리 언급하지 않는 한 널 객체를 인수로 받는 것을 기대하지 않습니다. API 함수는 널 객체를 반환할 수도 있습니다.
MLIR 객체는 C++에서 타입 계층을 형성할 수 있습니다. 예를 들어, 모든 타입을 나타내는 IR 클래스는 mlir::Type에서 파생되며, 이들 중 일부는 mlir::ShapedType이나 방언 전용의 공통 기반 클래스에서 추가로 파생될 수 있습니다. 타입 계층은 다음과 같은 명명 규칙을 통해 C API로 노출됩니다.
MlirType는 정의되지만 MlirShapedType는 정의되지 않습니다. 이는 파생 타입의 객체를 기반 타입을 기대하는 함수에 전달할 때 명시적인 업캐스팅이 필요하지 않도록 해 줍니다(핵심/표준 API에서 이런 경우가 더 흔하며, 다운캐스팅은 어차피 추가 검사를 수반합니다).X로부터 파생된 타입 Y는 int mlirXIsAY(MlirX) 함수를 제공하며, 주어진 동적 X 인스턴스가 Y의 인스턴스이기도 하면 0이 아닌 값을 반환합니다. 예: int MlirTypeIsAInteger(MlirType).Y를 사용하여 그 기대를 문서화합니다: MlirY<...>(MlirX, ...). 이 함수는 첫 번째 인수의 동적 인스턴스가 Y임을 단정(assert)하며, 이를 만족시키는 것은 호출자의 책임입니다.StringRef¶많은 MLIR 함수는 문자열의 비소유 조각을 가리키기 위해 StringRef 인스턴스를 반환합니다. 이 조각은 널로 종료되어 있을 수도 있고 아닐 수도 있습니다. C API에서는 이를 MlirStringRef 구조체 인스턴스로 표현하며, 문자열 조각의 첫 문자에 대한 포인터(str)와 조각의 길이(length)를 포함합니다. 조각은 반드시 널로 종료될 필요가 없으므로 마지막 문자를 식별하려면 length 필드를 사용해야 합니다. MlirStringRef는 비소유 포인터이며, 호출자가 복사를 수행하거나 가리키는 대상이 MlirStringRef의 모든 사용보다 오래 살아있도록 보장해야 합니다.
IR 객체는 mlirXPrint(MlirX, MlirStringCallback, void *) 함수를 사용해 출력할 수 있습니다. 이들 함수는 시그니처가 void (*)(const char *, intptr_t, void *)인 콜백과 사용자 정의 데이터에 대한 포인터를 인수로 받습니다. 프린터는 콜백을 호출하여 문자열 표현의 청크를 첫 문자에 대한 포인터와 길이로 제공하고, 사용자 정의 데이터는 수정 없이 그대로 전달합니다. 문자열 표현을 저장해야 한다면 메모리 할당과 복사는 호출자 책임입니다. 콜백에 넘겨지는 포인터가 널로 종료된 문자열을 가리킨다는 보장은 없으며, 문자열의 끝을 찾기 위해서는 크기 인수를 사용해야 합니다. 콜백은 문자열 표현의 연속된 청크로 여러 번 호출될 수 있습니다(출력 자체는 버퍼링됩니다).
이유: 이 접근 방식은 호출자가 할당을 완전히 제어하고 프린터 내부에서 불필요한 할당과 복사를 피할 수 있게 해줍니다.
편의를 위해, mlirXDump(MlirX) 함수가 제공되며 주어진 객체를 표준 오류 스트림으로 출력합니다.
API는 MLIR에서 반복적으로 나타나는 기능에 대해 다음과 같은 패턴을 채택합니다.
객체가 0부터 시작하는 연속된 정수 인덱스로 접근할 수 있는 필드(일반적으로 배열)를 가진다면, 그 객체는 인덱스 가능한 구성 요소를 가집니다. 예를 들어, MlirBlock은 인덱스 가능한 구성 요소로서 인자들을 가집니다. 하나의 객체는 이런 구성 요소를 여러 개 가질 수 있습니다. 예를 들어, MlirOperation은 속성, 피연산자, 리전, 결과, 후속자를 가집니다.
인덱스 가능한 구성 요소에는 다음과 같은 함수 쌍이 제공됩니다.
intptr_t mlirXGetNum<Y>s(MlirX)는 인덱스의 상한을 반환합니다.MlirY mlirXGet<Y>(MlirX, intptr_t pos)는 ‘pos’번째 하위 객체를 반환합니다.크기는 서명 있는 포인터 크기 정수(intptr_t)로 받거나 반환합니다. 이 typedef는 C99에서 사용할 수 있습니다.
함수 이름에서의 하위 객체 이름이 하위 객체의 실제 타입과 반드시 일치하는 것은 아닙니다. 예를 들어, mlirOperationGetOperand는 MlirValue를 반환합니다.
객체가 정수 인덱싱이 아닌 다른 순서(일반적으로 연결 리스트)를 통해 필드에 접근하는 이터레이터를 가진다면, 그 객체는 반복 가능한 구성 요소를 가집니다. 예를 들어, MlirBlock은 자신이 포함하는 연산들의 반복 가능한 리스트를 갖습니다. 하나의 객체는 여러 개의 반복 가능한 구성 요소를 가질 수 있습니다.
반복 가능한 구성 요소에는 다음과 같은 세 개의 함수가 제공됩니다.
MlirY mlirXGetFirst<Y>(MlirX)는 리스트의 첫 하위 객체를 반환합니다.MlirY mlirYGetNextIn<X>(MlirY)는 주어진 객체를 포함하는 리스트에서 다음 하위 객체를 반환하며, 주어진 객체가 이 리스트의 마지막이라면 널 객체를 반환합니다.int mlirYIsNull(MlirY)는 주어진 객체가 널이면 1을 반환합니다.함수에서 사용된 하위 객체의 이름이 그 타입과 일치할 수도 있고 그렇지 않을 수도 있습니다.
이 접근 방식은 다음과 같은 반복을 가능하게 합니다.
MlirY iter;
for (iter = mlirXGetFirst<Y>(x); !mlirYIsNull(iter);
iter = mlirYGetNextIn<X>(iter)) {
/* 'iter' 사용. */
}
방언 속성과 타입은 내장 속성과 타입의 예시를 따를 수 있습니다. 단, 구현은 별도의 디렉터리에 위치해야 합니다. 즉, include/mlir-c/<...>Dialect/ 및 lib/CAPI/<...>Dialect/입니다. 핵심 API는 include/mlir/CAPI/IR에 구현 비공개 헤더를 제공하며, 이를 통해 핵심 IR 구성 요소에 대한 불투명 C 구조체와 그 C++ 대응물 간 변환을 수행할 수 있습니다. wrap은 C++ 클래스를 C 구조체로 변환하고, unwrap은 그 반대 변환을 수행합니다. C++ 객체를 확보한 후에는, API 구현에서 mlirXIsAY를 구현하기 위해 isa에 의존하고, 다른 API 호출 내부에서는 cast를 사용하는 것이 기대됩니다.
인터페이스는 IR 인터페이스의 예시를 따를 수 있으며 적절한 라이브러리에 배치되어야 합니다(예: 공통 인터페이스는 mlir-c/Interfaces, 방언 전용 인터페이스는 해당 방언의 라이브러리). 다른 타입 계층과 마찬가지로, 인터페이스 자체 타입의 객체를 갖기보다는 최상위 객체들(MlirAttribute, MlirOperation, MlirType) 위에서 동작합니다. 정적 인터페이스 메서드는 선행 인수로 클래스의 정준 식별자(연산의 경우 이름을 담은 MlirStringRef, 속성과 타입의 경우 MlirTypeID)를 받고, 이어서 인터페이스가 등록된 MlirContext를 받는 것이 기대됩니다.
각 인터페이스는 mlir<InterfaceName>TypeID() 함수를 제공하는 것이 기대되며, 이를 통해 객체나 클래스가 이 인터페이스를 구현하는지 mlir<Attribute/Operation/Type>ImplementsInterface 또는 mlir<Attribute/Operation?Type>ImplementsInterfaceStatic 함수를 사용해 각각 확인할 수 있습니다. 이유: C++의 isa는 객체가 존재할 때만 동작하며, 정적 메서드는 보통 템플릿을 통해 디스패치됩니다. MLIRContext에서의 TypeID 기반 조회는 객체가 없어도 동작합니다.