ProIT: медіа для профі в IT
4 хв

PythoC: нові можливості генерації C-коду

author avatar ProIT NEWS

PythoC — це новий проєкт, який дає можливість використовувати Python як генератор C-коду, пропонуючи більше можливостей і гнучкості, ніж Cython. Це перший огляд нового інструмента генерації C-коду з Python, орієнтованого передусім на створення самостійних C-програм.

Python і C мають більше спільного, ніж може здаватися на перший погляд. Еталонна реалізація інтерпретатора Python написана мовою C, а багато сторонніх бібліотек для Python є обгортками навколо C-коду. Крім того, Python можна використовувати для генерації C-коду.

Зазвичай генерація C-коду з Python здійснювалася за допомогою бібліотек на кшталт Cython, які використовують Python-код з анотаціями типів для створення C-розширень, що імпортуються назад у Python.

PythoC застосовує інший підхід. Він використовує Python із підказками типів для програмної генерації C-коду, але насамперед для автономного використання, а також пропонує значно більше можливостей генерації коду на етапі компіляції, ніж Cython.

Автори PythoC описують свій підхід формулюванням «Python-керована компіляція». Проєкт перебуває на ранній стадії розвитку, але вже має достатньо працездатних функцій, щоб на нього звернули увагу.

Базова програма на PythoC

from pythoc import compile, i32

@compile
def add(x: i32, y: i32) -> i32:
    return x + y

if __name__ == "__main__":
    print(add(10, 20))

Щоб вказати, які функції модуля мають бути скомпільовані в C, використовується декоратор @compile з анотаціями типів для параметрів і результату. Зверніть увагу, що тут потрібно використовувати власний тип i32 з PythoC, а не стандартний int із Python. Це означає роботу з машинними цілими числами, а не з довільної довжини int у Python.

Під час запуску програма виведе 30, але з помітною затримкою. C-код компілюється на льоту під час кожного запуску. Наразі PythoC не має механізму повторного використання вже скомпільованого коду під час виклику з Python, на відміну від Cython.

На перший погляд це виглядає серйозним обмеженням, але саме в цьому й полягає ідея: PythoC призначений для генерації C-програм, які виконуються самостійно, а не для створення C-модулів, що імпортуються в Python.

Генерація автономних C-програм

from pythoc import compile, i32, ptr, i8
from pythoc.libc.stdio import printf

@compile
def add(x: i32, y: i32) -> i32:
    return x + y

@compile
def main(argc: i32, argv: ptr[ptr[i8]]) -> i32:
    printf("%u\n", add(10, 20))

if __name__ == "__main__":
    from pythoc import compile_to_executable
    compile_to_executable()

Функція compile_to_executable() компілює поточний модуль у виконуваний файл із такою самою назвою, включно зі всіма функціями, позначеними декоратором @compile.

Ще одна відмінність полягає в сигнатурі функції main(): вона відповідає стандартній функції main() у C. Це означає, що скомпільований виконуваний файл автоматично використовує її як точку входу.

Після запуску цього Python-скрипта створюється виконуваний файл у каталозі build, але він не запускається автоматично — його потрібно запускати окремо. Мета полягає в тому, щоб створити автономну C-програму, нічим не відмінну від написаної вручну на C, але з використанням синтаксису Python.

Емуляція можливостей C у PythoC

За нечисленними винятками PythoC здатен генерувати код, який повноцінно використовує можливості та модель виконання C.

Анотації типів дають можливість вказувати примітивні типи даних. Також можна використовувати анотацію ptr[T] для опису вказівників та array[T, N] для N-вимірних масивів типу T. Структури, об’єднання та переліки створюються шляхом декорування Python-класів. Усі стандартні оператори та керуючі конструкції працюють, за винятком goto. Для switch/case використовується match/case, але без підтримки fall-through.

Серед відсутніх можливостей — масиви змінної довжини. У C вони підтримуються лише з C11 і не є обов’язковими для компіляторів, тому відсутність цієї функції в PythoC наразі не виглядає несподіванкою.

Генерація коду на етапі компіляції

Cython також підтримує генерацію коду на етапі компіляції, зокрема створення різних варіантів C-коду або fallback на Python залежно від умов. Однак PythoC пропонує можливості, яких у Cython немає.

from pythoc import compile, struct, i32, f64

def make_point(T):
    @struct(suffix=T)
    class Point:
        x: T
        y: T

    @compile(suffix=T)
    def add_points(p1: Point, p2: Point) -> Point:
        result: Point = Point()
        result.x = p1.x + p2.x
        result.y = p1.y + p2.y
        return result

    return Point, add_points

Point_i32, add_i32 = make_point(i32)
Point_f64, add_f64 = make_point(f64)

Функція make_point(T) приймає анотацію типу (i32, f64) і на етапі компіляції створює спеціалізовані за типом версії класу Point і функції add_points. Параметр suffix у @compile змінює назву згенерованого об’єкта так, щоб тип був включений у назву, наприклад Point_i32 або Point_f64. У C це типовий спосіб розрізняти функції з різними сигнатурами. Також можливе поєднання цього механізму з диспетчеризацією під час виконання для реалізації поліморфізму.

Механізми безпеки пам’яті

Помилки, пов’язані з ручним керуванням пам’яттю в C, добре відомі. Cython має власні засоби для підвищення безпеки, але PythoC пропонує унікальні типоорієнтовані механізми.

Один із них — лінійні типи. Модуль linear дає можливість згенерувати доказ (proof), зазвичай пов’язаний із виділенням пам’яті, який має бути спожитий під час звільнення цієї пам’яті. Якщо для кожного prf=linear() немає відповідного consume(prf), то компілятор PythoC згенерує помилку на етапі компіляції.

У документації показано, як створити безпечні аналоги lmalloc() і lfree(). Використання лінійних типів не є обов’язковим, але вони дають можливість автоматизувати перевірки та перенести їх з етапу виконання на етап компіляції.

Ще один механізм — refinement types. Ідея полягає в тому, що можна визначити функцію для перевірки певної умови, наприклад перевірки вказівника на NULL, із булевим результатом. Потім за допомогою refine() значення передається в цю функцію і повертається тип refined[func]. Це змушує компілятор гарантувати, що такий тип буде оброблений у належний спосіб перед поверненням.

Таким чином поширені перевірки централізуються в одному місці. У Cython нічого подібного немає, оскільки його система типів переважно відтворює поведінку C.

Можливі напрями розвитку PythoC

PythoC є досить новим проєктом, тому його подальший розвиток поки відкритий. Один із потенційних напрямів — тісніша інтеграція з Python під час виконання. Наприклад, декоратор @cached міг би дозволяти компілювати модулі один раз наперед і повторно використовувати їх без повторної компіляції під час виклику з Python. Для цього, втім, знадобиться інтеграція з наявною системою збирання модулів Python.

Хоча такий рівень інтеграції може не входити до початкових цілей проєкту, він зробив би PythoC значно кориснішим для сценаріїв поєднання C і Python.

Читайте також на ProIT, як оновити свій Python-стек.

Підписуйтеся на ProIT у Telegram, щоб не пропустити жодної публікації!

Приєднатися до company logo
Продовжуючи, ти погоджуєшся з умовами Публічної оферти та Політикою конфіденційності.