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, щоб не пропустити жодної публікації!