درس ۲۲ از ۴۹ · برنامه‌های بزرگ‌تر

همکاری با C

سلام بدون هیچ کد واسط یا ابزار سازنده‌ی پوشش، مستقیم با کتابخانه‌های C کار می‌کند. یعنی کتابخانه‌ی استاندارد C و هر کتابخانه‌ی دیگری که به C نوشته شده از برنامه‌ی سلام در دسترس است.

درون‌داد

در بلوک درون‌داد: تابع‌های C را با گونه‌ی ورودی‌ها و خروجی‌شان اعلان کنید و بعد مثل هر تابع سلام صدایشان بزنید. این اعلان‌ها بدنه ندارند: کد در کتابخانه‌ی C است و سلام فقط باید بداند چطور صدایش بزند. نام تابع‌های C همان نام انگلیسی خودشان می‌ماند.

ffi-extern.salam
درون‌داد:
    تابع sqrt(x: اعشار۶۴): اعشار۶۴
    تابع abs(x: صحیح): صحیح
پایان

تابع ریشه:
    سرچاپ sqrt(۲.۰)
    سرچاپ abs(-۴۲)
پایان
خروجی1.4142135623730951 42

مترجم به این اعلان‌ها کاملاً اعتماد می‌کند. اگر اعلان با تابع واقعی C نخواند، مثلاً صحیح بنویسید جایی که C عدد double می‌خواهد، هیچ هشداری نمی‌بینید و برنامه داده‌ی بی‌معنا می‌خواند. پس امضای هر تابع را با دقت از سرآیند کتابخانه یا راهنمای آن بردارید.

گونه‌های هم‌ارز C

Cسلام
int، unsigned intصحیح (صحیح۳۲)، طبیعی (طبیعی۳۲)
long long، int64_t، uint8_tصحیح۶۴، صحیح۶۴، طبیعی۸
size_tاندازه مثبت
ssize_t، intptr_tاندازه
double، floatاعشار۶۴، اعشار۳۲
const char* که متن نگه می‌داردرشته
char* یا unsigned char* که بایت نگه می‌داردطبیعی۸* یا تهی*
void*، T*تهی*، T*
اشاره‌گر تابعتهی* یا درون‌داد تابع (...) R

رشته‌ی سلام از قبل به بایت صفر ختم می‌شود، پس می‌شود آن را مستقیم به هر تابع C که رشته می‌خواهد داد.

printf و تابع‌های با ورودی متغیر

تابع C که شمار ورودی‌هایش ثابت نیست، مثل printf، با ... پس از ورودی‌های ثابتش اعلان می‌شود. قاعده‌های قالب C برقرارند: %d برای صحیح، %f برای اعشار۶۴، %s برای رشته، و پهناهایی مثل %-8s و %6.2f برای ستون‌بندی:

ffi-printf.salam
درون‌داد:
    تابع printf(format: رشته، ...): صحیح
    تابع strlen(s: رشته): اندازه مثبت
    تابع atoi(s: رشته): صحیح
    تابع toupper(c: صحیح): صحیح
پایان

تابع ریشه:
    سرچاپ "رسید"
    printf("%-8s %3d x %6.2f\n"، "coffee"، ۲، ۳.۵)
    printf("%-8s %3d x %6.2f\n"، "bagel"، ۱، ۲.۲۵)
    شمار := atoi("12") + atoi("30")
    سرچاپ "اقلام:"، شمار
    واژه := "salam"
    سرچاپ "بایت‌ها:"، strlen(واژه)
    سرچاپ "حرف اول:"، نویسه‌ازکد(toupper(کدنویسه(واژه)))
پایان
خروجیرسید coffee 2 x 3.50 bagel 1 x 2.25 اقلام: 42 بایت‌ها: 5 حرف اول: S

خروجی printf و سرچاپ به همان ترتیبی می‌آید که برنامه نوشته است، چون سلام پیش از صدا زدن تابع C که چیزی می‌نویسد، بافر خروجی خودش را خالی می‌کند. فاصله‌گذاری %-8s بایت‌ها را می‌شمارد نه حرف‌ها را، و هر حرف فارسی دو بایت است؛ برای همین در این نمونه نام کالاها لاتین است. برای ستون‌بندی متن فارسی از سرچاپ یا بسته‌ی قالب بندی استفاده کنید.

اشاره‌گر، بافر و بایت صفر

بسیاری از تابع‌های C در حافظه‌ای که به آن‌ها می‌دهید می‌نویسند. این حافظه را با بسته‌ی حافظه بگیرید، اشاره‌گرش را بدهید و وقتی کارتان تمام شد آزادش کنید؛ یک دیرکن درست پس از گرفتن حافظه نمی‌گذارد فراموش شود. اشاره‌گر را مثل آرایه شماره بزنید: بافر[ای] یک خانه را می‌خواند یا می‌نویسد.

ffi-buffer.salam
واردسازی حافظه

درون‌داد:
    تابع snprintf(buf: تهی*، size: اندازه مثبت، format: رشته، ...): صحیح
    تابع getenv(name: رشته): رشته
پایان

تابع ریشه:
    بافر := حافظه.تخصیص صفرشده(۳۲ برگردان طبیعی۶۴) برگردان طبیعی۸*
    دیرکن حافظه.آزادسازی(بافر برگردان تهی*)

    ن := snprintf(بافر برگردان تهی*، ۳۲ برگردان اندازه مثبت، "%s-%05d"، "INV"، ۴۲)
    برچسب := بافر برگردان رشته
    سرچاپ ن، برچسب

    بایت‌ها := [۷۲، ۱۰۵، ۰، ۳۳، ۳۳]
    تکرار ۵ در ای:
        بافر[ای] = بایت‌ها[ای] برگردان طبیعی۸
    پایان
    متن := بافر برگردان رشته
    سرچاپ "به‌صورت رشته:"، طول(متن)، "بایت"
    ناپایا جمع := ۰
    تکرار ۵ در ای:
        جمع += بافر[ای] برگردان صحیح
    پایان
    سرچاپ "از راه اشاره‌گر:"، جمع

    مقدار := getenv("SALAM_EXAMPLE_UNSET")
    اگر (مقدار برگردان تهی*) == پوچ:
        سرچاپ "SALAM_EXAMPLE_UNSET تعریف نشده است"
    پایان
پایان
خروجی9 INV-00042 به‌صورت رشته: 2 بایت از راه اشاره‌گر: 243 SALAM_EXAMPLE_UNSET تعریف نشده است

به خط وسط خروجی دقت کنید. رشته در نخستین بایت صفر تمام می‌شود، پس پنج بایت H i 0 ! ! به رشته‌ی دوبایتی Hi تبدیل می‌شوند و هیچ خطایی هم دیده نمی‌شود؛ باقی بایت‌ها بی‌صدا از دست می‌روند. متنی که از C می‌آید را می‌شود رشته کرد، اما داده‌ی دودویی مثل تصویر، پرونده‌ی فشرده یا بسته‌ی شبکه را نه. داده‌ی دودویی را به شکل اشاره‌گر و طول نگه دارید و از راه اشاره‌گر بخوانید، همان کاری که حلقه‌ی جمع هر پنج بایت می‌کند.

تابع C که ممکن است رشته‌ای برنگرداند، مثل getenv برای متغیری که تعریف نشده، اشاره‌گر تهی برمی‌گرداند. پیش از استفاده آن را با (مقدار برگردان تهی*) == پوچ بسنجید. رشته‌ای که C برمی‌گرداند مال کتابخانه‌ی C است؛ هرگز خودتان آزادش نکنید.

تابع‌های کتابخانه‌ی استاندارد C چیز دیگری لازم ندارند. برای هر کتابخانه‌ی دیگر، با پیوند پیش از بلوک درون‌داد به پیونددهنده بگویید کدام را بردارد. نوع پیوند همیشه نوشته می‌شود:

  • پیوند پویا "sqlite3" کتابخانه‌ی اشتراکی را پیوند می‌دهد، مثل -lsqlite3 در ساخت C
  • پیوند ایستا "z" کتابخانه‌ی ایستای libz.a را می‌خواهد که درون برنامه کپی می‌شود
  • پیوند چارچوب "Cocoa" یک چارچوب مک را پیوند می‌دهد

پیوند "m" بدون نوع خطاست و پیامش هر سه نوع را نام می‌برد. چارچوب فقط روی مک وجود دارد، پس آن را درون یک اگر زمان ترجمه بگذارید، مثل این نمونه که کتابخانه‌ی ریاضی C را پیوند می‌دهد:

ffi-link.salam
پیوند پویا "m"

اگر سلام سیستم مک:
    پیوند چارچوب "CoreFoundation"
پایان

درون‌داد:
    تابع fmod(x: اعشار۶۴، y: اعشار۶۴): اعشار۶۴
    تابع hypot(x: اعشار۶۴، y: اعشار۶۴): اعشار۶۴
    تابع round(x: اعشار۶۴): اعشار۶۴
پایان

تابع ریشه:
    سرچاپ "باقی‌مانده‌ی ۱۷ دقیقه در بازه‌های ۵ دقیقه‌ای:"، fmod(۱۷.۰، ۵.۰)
    سرچاپ "قطر صفحه‌ی ۳ در ۴:"، hypot(۳.۰، ۴.۰)
    سرچاپ "گرد شده‌ی ۲.۵:"، round(۲.۵)
پایان
خروجیباقی‌مانده‌ی ۱۷ دقیقه در بازه‌های ۵ دقیقه‌ای: 2 قطر صفحه‌ی ۳ در ۴: 5 گرد شده‌ی ۲.۵: 3

برون‌داد: تابع‌های سلام برای C

بعضی تابع‌های C شما را صدا می‌زنند. qsort آرایه‌ای از هر گونه‌ای را مرتب می‌کند و برای مقایسه‌ی دو خانه از تابعی که شما می‌دهید کمک می‌گیرد. تابع سلامی که C باید صدایش بزند در بلوک برون‌داد: می‌آید؛ آن وقت یک تابع واقعی C با همان نام ساده است و &نام نشانی آن است که جایی که C اشاره‌گر تابع می‌خواهد داده می‌شود:

ffi-qsort.salam
واردسازی حافظه

درون‌داد:
    تابع qsort(base: تهی*، count: اندازه مثبت، width: اندازه مثبت، compare: تهی*)
پایان

برون‌داد:
    تابع by_score(الف: تهی*، ب: تهی*): صحیح:
        ایکس := (الف برگردان صحیح۳۲*)[۰]
        ایگرگ := (ب برگردان صحیح۳۲*)[۰]
        اگر ایکس < ایگرگ:
            برگشت -۱
        وگرنه ایکس > ایگرگ:
            برگشت ۱
        پایان
        برگشت ۰
    پایان
پایان

تابع ریشه:
    نمره‌ها := [۴۲، ۷، ۱۹، ۸۸، ۳]
    ن := طول(نمره‌ها)
    پ := حافظه.تخصیص((ن * ۴) برگردان طبیعی۶۴) برگردان صحیح۳۲*
    دیرکن حافظه.آزادسازی(پ برگردان تهی*)
    تکرار ن در ای:
        پ[ای] = نمره‌ها[ای]
    پایان

    qsort(پ برگردان تهی*، ن برگردان اندازه مثبت، ۴ برگردان اندازه مثبت، &by_score)

    ناپایا سطر := ""
    تکرار ن در ای:
        سطر += " " + پ[ای]
    پایان
    سرچاپ "مرتب‌شده:" + سطر
پایان
خروجیمرتب‌شده: 3 7 19 42 88

تابع مقایسه دو اشاره‌گر تهی* به دو خانه می‌گیرد، آن‌ها را به صحیح۳۲* برمی‌گرداند و با [۰] می‌خواند. عددها باید در حافظه‌ای باشند که C به آن دسترسی دارد، برای همین برنامه آن‌ها را در بافری از بسته‌ی حافظه کپی می‌کند، می‌گذارد qsort همان‌جا مرتبشان کند و نتیجه را از راه اشاره‌گر می‌خواند.

تابع برون‌داد همیشه یک نماد عمومی و معمولی C است و این قاعده‌ها را دارد:

  • هر تابع بلوک باید بدنه داشته باشد؛ اعلان بی‌بدنه جایش در درون‌داد است
  • متغیر، تابع عام و ورودی ... مجاز نیست
  • همگانی، درخط و نادرخط مجاز نیست؛ فقط بی‌کاره، ناب و نابرگشت
  • نام باید یک‌کلمه‌ای باشد، چون همان نام C می‌شود. با حروف لاتین بنویسیدش: پشتیبان LLVM مترجم هنوز نمی‌تواند نام برون‌داد غیرلاتین بسازد

صدا زدن از راه اشاره‌گر تابع C

جهت دیگر هم کار می‌کند. (&نام) برگردان درون‌داد تابع (اعشار۶۴) اعشار۶۴ نشانی یک تابع C، یا یک تابع برون‌داد سلام، را به مقداری گونه‌دار تبدیل می‌کند که می‌شود نگهش داشت، جابه‌جایش کرد و صدایش زد. درون‌داد تابع (...) R گونه‌ی ورودی هم هست، پس یک تابع سلام می‌تواند با هر تابع C به همان شکل کار کند:

ffi-pointer.salam
درون‌داد:
    تابع sqrt(x: اعشار۶۴): اعشار۶۴
    تابع cbrt(x: اعشار۶۴): اعشار۶۴
پایان

برون‌داد:
    تابع halve(x: اعشار۶۴): اعشار۶۴:
        برگشت x / ۲.۰
    پایان
پایان

تابع نمایش(نام: رشته، ر: درون‌داد تابع (اعشار۶۴) اعشار۶۴، ع: اعشار۶۴):
    سرچاپ نام + "(" + ع + ") ="، ر(ع)
پایان

تابع ریشه:
    نمایش("sqrt"، (&sqrt) برگردان درون‌داد تابع (اعشار۶۴) اعشار۶۴، ۶۴.۰)
    نمایش("cbrt"، (&cbrt) برگردان درون‌داد تابع (اعشار۶۴) اعشار۶۴، ۶۴.۰)
    نمایش("halve"، (&halve) برگردان درون‌داد تابع (اعشار۶۴) اعشار۶۴، ۶۴.۰)
پایان
خروجیsqrt(64) = 8 cbrt(64) = 4 halve(64) = 32

نام یک تابع معمولی سلام وقتی به‌عنوان مقدار به کار رود، نشانی آن به شکل صحیح۶۴ است، همان چیزی که جدول‌های فراخوان مثل مسیریاب وب می‌گیرند. برای جای تهی* یا برای تبدیل به تابع C گونه‌دار، &نام بنویسید.

در ویندوز ۳۲ بیتی، تابع‌های Win32 با قرارداد فراخوانی stdcall کار می‌کنند. اشاره‌گر به چنین تابعی، مثلاً نشانی‌ای که GetProcAddress برمی‌گرداند، گونه‌ی درون‌داد فراخوان استاندارد تابع (...) R می‌گیرد تا فراخوانی با آن جور باشد. روی هر هدف دیگری این واژه چیزی را تغییر نمی‌دهد.

چند قاعده‌ی سرانگشتی

  • امضای هر تابع C را دقیق بنویسید؛ مترجم نمی‌تواند آن را بسنجد.
  • برای متن از رشته و برای بایت از اشاره‌گر و طول استفاده کنید.
  • هر چه با بسته‌ی حافظه می‌گیرید آزاد کنید، و چیزی را که C برمی‌گرداند آزاد نکنید مگر راهنمایش بگوید.
  • اشاره‌گری را که C ممکن است تهی برگرداند پیش از استفاده بسنجید.
  • وقتی کتابخانه‌ی استاندارد سلام کاری را انجام می‌دهد همان را به کار ببرید: صدا زدن C را برایتان انجام می‌دهد و روی همه‌ی سیستم‌ها یکسان کار می‌کند.

گاهی هر سیستم تابع C متفاوتی لازم دارد. بلوک درون‌داد را می‌شود درون یک اگر سلام سیستم ویندوز: سطح بالا گذاشت؛ درس ترجمه‌ی شرطی را ببینید.