رفتن به مطلب
مرجع رسمی سی‌پلاس‌پلاس ایران

برنامه نویسی

  • نوشته‌
    21
  • دیدگاه
    19
  • مشاهده
    9,462

مشارکت‌کنندگان این وبلاگ

درباره این وبلاگ

نوشته‌های این وبلاگ

کامبیز اسدزاده

فرق بین کامپایل استاتیک و داینامیک

قبل از اینکه فرق بین ایستا (استاتیک) - Static و پویا (داینامیک) - Dynamic را بدانیم لازم است در رابطه با چرخهٔ زندگی نوشتن یک برنامه و اجرای آن آشنا شویم. هر برنامه برای اولین بار توسط یک محیط توسعه (Editor) یا IDE توسط برنامه‌نویسان انتخاب و به صورت فایل متنی قابل ویرایش می‌باشد. سپس فایل متنی که شامل کد‌های نوشته شده توسط برنامه‌نویس تحت زبان برنامه‌نویسی مانند C، C++ و غیره... می‌باشد توسط کامپایلر به کد شیء ای تبدیل می‌شود که ماشین بتواند آن را درک کرده و اجرا کند.

برنامه ای که ما می‌نویسیم ممکن است به عنوان یک مورد توسط دیگر برنامه ها یا کتابخانه‌هایی از برنامه ها مورد استفاده قرار بگیرد برقراری ارتباط (پیوند‌کردن - لینکر) یا همان لینک کردن پروسه‌ای است که برای اجرای موفقیت آمیز برنامه‌های نوشته شده ما بکار می‌رود؛ برقراری ارتباط بین ایستا و پویا دو پروسه‌ای از جمع‌آوری و ترکیب فایل‌های شیء‌های مختلفی است که به منظور ایجاد یک فایل اجرایی می‌باشند. در این بخش ما تصمیم بر این داریم تا تفاوت بین آن ها را با جزئیات مورد بررسی قرار دهیم.

عمل پیوند یا ترکیب در زمان کامپایل انجام شود، در واقع زمانی که کد منبع به زبان ماشین ترجمه می‌شود، در زمان بارگذاری، زمانی که برنامه در داخله حافظه بارگذاری می‌شود، و حتی زمان اجرای آن توسط برنامه صورت می‌گیرد این عمل زمان پیوند و یا ترکیب (اتصال) است. در نهایت این فرآیند توسط برنامه ای اجرا می شود که به آن لینکر - پیوند دهنده (ترکیب کننده) می‌گویند. اتصال دهنده ها به عنوان ویرایستار لینک نیز معرفی می‌شوند. لینک شدن (پیوند شدن) به آخرین مرحله از کامپایل می‌گویند.

در زبان علمی اصطلاح لینکر یا Linker معروف است اما در زبان فارسی بهترین گزینه مربوطه را می‌توان با عنوان اتصال دهنده، پیوند دهنده، ترکیب کننده نام برد. همه آن ها نشانگر یک هدف به منظور ترکیب اشیاء با یکدیگر هستند که در مرحله کامپایل صورت می‌گیرد.

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

به طور خلاصه روند زیر می‌تواند به عنوان چرخه زندگی یک برنامه خلاصه شود (نوشتن - لینک کردن - بارگذاری - اجرا)

فرق بین کامپایل استاتیک و داینامیک

program-life-cycle.png

در زیر تفاوت های عمده ارتباط بین استاتیک و داینامیک آورده شده است :

استاتیک 

  • ارتباط به روش استاتیک فرآیندی است که تمامی ماژول‌ها و کتابخانه‌های برنامه در فایل اجرایی نهایی کپی می‌شوند. این روش توسط لینکر در مرحله آخر کامپایل انجام می‌شود. اتصال دهنده - لینکر طبق روال ترکیبی کتابخانه ها را با کد برنامه و همراه مراجع - منابع خارجی ترکیب کرده و برای تولید یک بارگذاری مناسب در حافظه آماده سازی می‌کند. زمانی که برنامه بار‌گذاری می‌شود، سیستم عامل محلی را در حافظه به صورت یک فایل اجرایی که شامل کد‌های اجرایی و داده ها می‌باشد مشخص می‌کند.
  • ارتباط به شیوهٔ استاتیک توسط برنامه‌ای با نام لینکر انجام می‌شود که در آخرین مرحله فرآیند کامپایل یک برنامه صورت می‌گیرد. لینکر‌ها نیز به عنوان ویرایشگر پیوند نیز عنوان می‌شوند.
  • فایل های استاتیک به طور قابل توجهی دارای اندازه بسیار بزرگی هستند زیرا برنامه های خارجی و کتابخانه های لینک شده همه در یکجا و در فایل نهایی اجرایی جمع آوری شده‌اند.
  • در اتصال استاتیک اگر هر یک از برنامه های خارجی تغییر کرده باشد باید آن ها دوباره کامپایل شوند و مجددا عمل اتصال صورت گیرد در غیر اینصورت هیچ تغییری در به روز رسانی های مرتبط با فایل اجرایی مشاهده نخواهد شد.
  • برنامه‌های استاتیکی زمان بارگذاری ثابتی در هر بار اجرای برنامه در حافظه را در نظر می‌گیرند. و زمانی که برای بارگذاری طول می کشد ثابت است.
  • برنامه‌هایی که از کتابخانه‌های استاتیکی استفاده می‌کنند معمولاً سریعتر از برنامه‌هایی هستند که کتابخانه‌‌ی آن‌ها به صورت پویا می‌باشد.
  • در برنامه های استاتیکی، تمامی کد ها شامل یک فایل اجرایی می‌باشند. بنابراین، آن‌ها هرگز در برنامه هایی که دارای مشکلاتی هستند اجرا نخواهند شد.

داینامیک

  • در ارتباط پویا نام کتابخانه های خارجی (کتابخانه‌های به اشتراک گذاری شده) در فایل اجرایی نهایی قرار داده شده‌اند نه خود کتابخانه. در حالی که ارتباط واقعی در زمان اجرا در هر دو فایل در حافظه قرار می‌گیرند. اتصال پویا این اجازه را می‌دهند تا برنامه های متعددی به صورت یک ماژول کپی شده و قابل اجرا مورد استفاده قرار بگیرد.
  • اتصال پویا بر خلاف اتصال استاتیک در زمان اجرا توسط سیستم عامل انجام می‌شود.
  • در اتصال پویا فقط یک نسخه از کتابخانه به اشتراک گذاری شده در حافظه نگه‌داری می‌شود. این به طور قابل توجهی اندازه برنامه های اجرایی را کاهش می‌دهد، در نتیجه صرفحه جویی در حافظه و فضای دیسک صورت خواهد گرفت.
  • در اتصال پویا بر خلاف اتصال استاتیک نیازی به کامپایل کامل پروژه نمی‌باشد در صورتی که لازم باشد تغییراتی در هر یک از فایل‌ها صورت بگیرد تنها کافی است آن را کامپایل و در کنار برنامه قرار دهید. این یکی از بزرگترین مزیت‌های کامپایل داینامیکی می‌باشد.
  • در اتصال پویا زمان بارگذاری برنامه در حافظه ممکن است کاهش یابد. این در صورتی است که کتابخانه های مشترک در حافظه بارگذاری شده‌اند.
  • برنامه‌هایی که از کتابخانه های مشترک استفاده می‌کنند معمولا کندتر از برنامه هایی هستند که از کتابخانه های استاتیکی استفاده می‌کنند.
  • برنامه‌های پویا وابسته به داشتن کتابخانه‌های سازگار هستند. اگر کتابخانه تغییر یابد (برای مثال، یک کامپایلر جدید منتشر شود ممکن است کتابخانه را تغییر دهد)، در این صورت ممکن است برنامه مجدداً تحت کتابخانه جدید باز نویسی و به‌روز رسانی شوند. اگر کتابخانه از روی سیستم حذف شود، برنامه‌ای که وابسته آن کتابخانه می‌باشد دیگر کار نخواهد کرد.

در ادامه شما می‌توانید در مورد مراحل کامپایل یک برنامه مراجعه کنید:

 

کامبیز اسدزاده

یکی از مواردی که در مباحث شیء‌گرایی مهم هستن با عنوان اصول SOLID شناخته می‌شه که شاید خیلی‌ها شنیده باشند.

واژهٔ SOLID برگرفته شده از پنج اصلِ زیر است:

  1. S - Single-responsiblity Principle
  2. O - Open-closed Principle
  3. L - Liskov Substitution Principle
  4. I - Interface Segregation Principle
  5. D - Dependency Inversion Principle

68747470733a2f2f6d69726f2e6d656469756d2e636f6d2f6d61782f313139312f312a4f7a774152627648556731526c5a374c59794c4372672e706e67.png

 

برای پیاده‌سازی SOLID در C++20، می‌توانید از ویژگی‌های زبان استفاده کنید. برای مثال:

  •  برای پیاده‌سازی SRP، می‌توانید از کلاس‌های ساختاری (Structs) و کلاس‌ها و متد‌ها که فقط یک وظیفه دارند، استفاده کنید.
  •  برای پیاده‌سازی OCP، می‌توانید از الگوی Visitor و Strategy استفاده کنید. این باعث می‌شود که کلاس‌های شما قابلیت بسته‌بودن (Close) و در عین حال قابلیت گسترش ‌(Open) را داشته باشند.
  •  برای پیاده‌سازی LSP، می‌توانید از وراثت، کلاس‌های پایه (Base classes) و کلاس‌های مشتق (Derived classes) استفاده کنید و مطمئن شوید که اصول وراثت رعایت شده است.
  •  برای پیاده‌سازی ISP، می‌توانید از الگوی Interface استفاده کنید که به شما امکان می‌دهد که کلاس‌ها فقط به آن قسمت‌هایی از یک Interface نیاز دارند که به آن‌ها لازم است و از بقیه صرف نظر کنند.
  •  برای پیاده‌سازی DIP، می‌توانید از Dependency Injection استفاده کنید که به شما این امکان را می‌دهد که از تمام وابستگی‌های بین کلاس‌ها جدا شده و به‌صورت جداگانه به هم پیوندید، به‌طوری‌که تغییر در یک کلاس اثرات اصلی بر سایر کلاس‌ها نداشته باشد.

اصلِ اول مربوط به اصل Single-Responsibility Principle یا همان SRP است. این اصل مشخص می‌کند که کلاس‌های شما باید هر کدامشان فقط و فقط باید یک وظیفهٔ مشخص داشته باشند و نه بیشتر!

 

برای پیاده‌سازی SRP

بنابراین، اصلِ SRP در شیء‌گرایی به معنی این است که هر کلاس و متد باید فقط یک مسئولیت یا وظیفه را برعهده داشته باشد. این یعنی که هر کلاس فقط باید یک مورد از نرم‌افزار را انجام دهد و تغییر در آن، صرفا برای اعمال تغییرات در آن مورد خاص یا در راستای بهبود مسئولیتش باشد.

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

برای پیاده‌سازی SRP در سی‌پلاس‌پلاس مثالی خواهم زد؛ ابتدا باید کلاس را به شکلی طراحی کنید که فقط یک مسئولیت را در بر داشته باشد. به عنوان مثال، فرض کنید یک کلاس برای محاسبه‌ٔ مساحت یک شکل هندسی طراحی می‌کنید. با توجه به SRP، این کلاس فقط باید مسئول محاسبه‌ٔ مساحت باشد و هیچ وظیفه‌ای دیگر را نباید برعهده بگیرد.

class Shape {
public:
    virtual double area() const = 0;
};

class Rectangle : public Shape {
public:
    Rectangle(double h, double w) : height(h), width(w) {}
    double area() const override {
        return height * width;
    }
private:
    double height;
    double width;
};

class Circle : public Shape {
public:
    Circle(double r) : radius(r) {}
    double area() const override {
        return 3.1415 * radius * radius;
    }
private:
    double radius;
};

در این مثال، کلاس Shape یک مسئولیت واحد دارد که محاسبه‌ٔ مساحت شکل هندسی است. همینطور کلاس‌های Rectangle و Circle نیز فقط مسئول محاسبه‌ٔ مساحت هر یک از شکل‌های هندسی خود هستند.

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

 

برای پیاده‌سازی OCP

اصلِ Open/Closed Principle (اصل OCP) در شیء‌گرایی به معنی ایجاد کلاس‌ها به گونه‌ای است که برای اضافه کردن ویژگی جدید به یک برنامه، نیاز به تغییر کد قبلی نباشد. به عبارت دیگر، کلاس‌ها باید برای توسعه باز باشند (Open)، اما برای تغییر نباشند (Closed). برای پیاده‌سازی OCP، می‌توانید از ارث‌بری، پلی‌مورفیسم، استفاده از ابستراکت کلاس‌ها (کلاس‌های انتزاعی)، الگوی تزریق وابستگی و ... استفاده کنید. این اصل بهبود پذیری (extensibility) و بهره‌وری کد را بهبود می‌بخشد. همچنین برای پیاده‌سازی OCP، می‌توانید از الگوی Visitor و Strategy استفاده کنید. این باعث می‌شود که کلاس‌های شما قابلیت بسته‌بودن (Close) و در عین حال قابلیت گسترش ‌(Open) را داشته باشند.

به عنوان مثال، فرض کنید یک برنامه داریم که می‌تواند اشکال هندسی مختلف را رسم کند. برای پیاده‌سازی OCP، می‌توانید کلاس اشکال هندسی را به گونه‌ای طراحی کنید که بتوانید به راحتی از آن‌ها ارث‌بری کنید و اشکال جدیدی را به سیستم اضافه کنید بدون ایجاد تغییرات بیشتر در کد قبلی. به عنوان مثال، اینجا یک کد ساده برای این کار نوشته شده است:

#include <iostream>
#include <vector>

class Shape {
public:
    virtual void draw() = 0;
};

class Rectangle : public Shape {
public:
    void draw() override {
        std::cout << "Drawing a rectangle\n";
    }
};

class Circle : public Shape {
public:
    void draw() override {
        std::cout << "Drawing a circle\n";
    }
};

class GraphicEditor {
public:
    void drawAllShapes(std::vector<Shape*> shapes) {
        for (auto shape : shapes) {
            shape->draw();
        }
    }
};

int main() {
    GraphicEditor editor;
    std::vector<Shape*> shapes = { new Rectangle(), new Circle() };
    editor.drawAllShapes(shapes);
    for (auto shape : shapes) {
        delete shape;
    }
    return 0;
}

در این مثال، کلاس Shape در واقع یک واسط برای هر یک از شکل‌های هندسی است. کلاس Rectangle و کلاس Circle از کلاس Shape ارث‌بری کرده‌اند و تابع draw آن را پیاده‌سازی کرده‌اند. به این ترتیب، می‌توانید در آینده اشکال هندسی جدیدی را به کد اضافه کنید بدون نیاز به تغییر کد اصلی.

 

برای پیاده‌سازی LSP

اصل LSP به معنی اصل جایگزینی قابل توجه لیسکوف (Liskov Substitution Principle) در شیء‌گرایی به این مفهوم است که باید بتوان اشیاء موروثی از یک کلاس را با اشیاء کلاس والد جایگزین کرد. به عبارت دیگر، هر جایگزین یا زیرکلاس باید بتواند عملکرد و ویژگی‌های کلاس والد را حفظ کند و بدون هیچ تغییری، به جای کلاس والد استفاده شود. در واقع، هدف این اصل این است که به جای ایجاد جایگزین‌هایی که منجر به عملکرد غیرقابل پیش‌بینی می‌شوند، جایگزین‌هایی باشند که با انجام کارهایی مانند جایگزینی هنوز هم به نحو شایسته عمل کنند.

فرض کنید که گربه و سگ، از یک کلاس حیوان وارثی هستند. برای همه‌ٔ حیوانات منطقی است که بتوانند صدای حیوان را تولید کنند. در اینجا با توجه به اصل LSP، باید بتوانیم به جای یک شیء گربه، یک شیء سگ یا شیء کلاس والدِ حیوان را به عنوان جایگزین استفاده کنیم و همچنین از آن‌ها بخواهیم که قابلیت تولید صدا را داشته باشند. به عبارت دیگر، صدای گربه و صدای سگ برای ما دقیقاً در یک رده‌بندی هستند و در همان حیطه معنایی قرار دارند، بنابراین باید بتوانیم هر یک از آن‌ها را به عنوان جایگزین برای دیگری استفاده کنیم.

یک مثال ساده از اصل LSP در C++20، می‌تواند مربوط به کلاس‌های Shape و Rectangle باشد. فرض کنید کلاس Rectangle از کلاس Shape و از آن ارث‌بری کرده باشد. برای رعایت اصل LSP، کلاس Rectangle باید تمام روش‌های کلاس Shape را پیاده‌سازی کند، به گونه‌ای که در هر جایی که در کد از کلاس Shape استفاده می‌شود، می‌توانیم جایگزین آن را با کلاس Rectangle استفاده کنیم.

کلاس Shape می‌تواند به صورت زیر باشد:

class Shape {
public:
    virtual double area() = 0;
    virtual double perimeter() = 0;
};

و کلاس Rectangle از شکل یک مستطیل با طول و عرض دلخواه پیاده‌سازی شده است:

class Rectangle : public Shape {
private:
    double length;
    double width;
public:
    Rectangle(double l, double w) : length(l), width(w) {}
    double area() override {
        return length * width;
    }
    double perimeter() override {
        return 2 * (length + width);
    }
};

حال، با استفاده از این کلاس‌ها، می‌توانیم مستطیلی با طول و عرض دلخواه ایجاد کنیم و روش‌های کلاس Shape را روی آن صدا بزنیم:

Shape* shapePtr = new Rectangle(4, 6);
double area = shapePtr->area();
double perimeter = shapePtr->perimeter();

در اینجا، کلاس Rectangle با کلاس Shape جایگزین شده و اصل LSP رعایت شده است، به این معنی که هر جایی که از کلاس Shape استفاده شده، می‌توانیم جایگزین آن را با کلاس Rectangle استفاده کنیم، بدون هیچ تغییری در کد قبلی.

 

برای پیاده‌سازی ISP

اصل ISP به معنی اصل جداسازی رابط (Interface Segregation Principle) در شیء‌گرایی به این مفهوم است که باید رابط‌ها را به گونه‌ای طراحی کرد که اشیاء فقط به آنچه برایشان لازم است دسترسی داشته باشند و به سایر اجزای رابط دسترسی نداشته باشند. به عبارت دیگر، باید یک رابط یکتا و بزرگ به چندین رابط کوچک و مجزا تفکیک شود تا برای هر کلاس، فقط تعداد کم و لازمی از ویژگی‌ها و روش‌ها در دسترس باشد. 

برای پیاده‌سازی ISP، می‌توانید از الگوهای طراحی مانند واسط‌ها (Interfaces) و کلاس‌های واسط (Abstract Classes) استفاده کنید. با استفاده از این الگوها، می‌توانید پیچیدگی‌های شناور در برنامه خود را کاهش داده و تغییرات را در کد خود به راحتی انجام دهید.

در واقع، هدف این اصل این است که کلاینت‌ها باید بتوانند با استفاده از رابط‌های ساده تر و متعارف، با سیستم تعامل داشته باشند. این روش منجر به افزایش قابلیت توسعه و خودکارسازی کد، بهره‌وری بالا و حتی صرفه‌جویی در زمان و هزینه خواهد شد. برای مثال نمونهٔ زیر به عنوان بخشی از پردازش تصویر می‌باشد:

ابتدا واسط ImageTransformer را تعریف می‌کنیم که حاوی متدهای تبدیل تصویر به سیاه و سفید و برعکس آن است:

class ImageTransformer {
public:
   virtual void transformToBlackAndWhite() = 0;
   virtual void transformToColor() = 0;
};

سپس دو کلاس ImageToBlackAndWhiteTransformer و ImageToColorTransformer را برای پیاده‌سازی واسط ImageTransformer تعریف می‌کنیم:

class ImageToBlackAndWhiteTransformer: public ImageTransformer {
public:
    void transformToBlackAndWhite() override {
        // Implement the transformation to black and white
    }
};

class ImageToColorTransformer: public ImageTransformer {
public:
    void transformToColor() override {
        // Implement the transformation to color
    }
};

حال می‌توانیم از هر یک از کلاس‌های ImageToBlackAndWhiteTransformer و ImageToColorTransformer برای پردازش تصویر استفاده کنیم.

در نهایت، به عنوان مثالی از استفاده از متد‌های رابط ImageTransformer، کد زیر را در نظر بگیرید:

void processImage(ImageTransformer& transformer) {
    transformer.transformToBlackAndWhite();
    transformer.transformToColor();
}

در این کد، با گرفتن یک شیء از کلاسی که از واسط ImageTransformer ارث‌بری کرده است، می‌توانیم تصویر را به سیاه و سفید و برعکس آن تبدیل کنیم که بر اساس اصل ISP طراحی شده‌است.

برای پیاده‌سازی اصل ISP در C++20 به‌طور کلی، می‌توان از این ویژگی بهره برد که به نام concepts شناخته می‌شود. این ویژگی به برنامه نویسان امکان می‌دهد که شرایط و محدودیت‌هایی را برای قالب‌ها، ورودی‌ها و خروجی‌ها تعریف کنند و باعث شود که کد بهتری با پایداری بیشتری نوشته شود.

برای استفاده از این ویژگی در پیاده‌سازی اصل ISP در C++20، می‌توانید از مفهومی استفاده کنید که برای اطمینان از قابلیت اجرا و خطاهای برنامه سازی موجود است. برای مثال، در کد زیر، الگوهای واسط که برای پردازش تصویر استفاده می‌شوند، با نام‌های ImageTransformer و GrayscaleTransformer (با استفاده از template) تعریف شده‌اند. هر کدام از این الگوهای واسط، تعدادی عملیات مشخص شده را تعریف می کنند. سپس با استفاده از این الگوها، کلاس ImageProcessor (نیز با استفاده از template) برای پردازش تصویر طراحی شده است.

template<typename T>
concept ImageTransformer = requires(T t, cv::Mat image) {
    { t.transform_to_grayscale(image) } -> cv::Mat;
    { t.transform_to_rgb(image) } -> cv::Mat;
};

template<typename T>
concept GrayscaleTransformer = requires(T t, cv::Mat image) {
    { t.transform_to_grayscale(image) } -> cv::Mat;
};

template <typename T>
class ImageProcessor {
public:
    ImageProcessor(T* transformerPtr) : transformerPtr_{ transformerPtr } { }
    
    cv::Mat transform_to_grayscale(cv::Mat image) {
        return transformerPtr_->transform_to_grayscale(image);
    }
    
    cv::Mat transform_to_rgb(cv::Mat image) {
        return transformerPtr_->transform_to_rgb(image);
    }
    
private:
    T* transformerPtr_;
};

همچنین، می‌توانید کلاس‌های مربوط به پردازش تصویر یعنی GrayscaleImageTransformer و RGBImageTransformer را به این الگوها منطبق کنید. در کد زیر، چک کردن اینکه یک کلاس منطبق بر پیشنیازهای ImageTransformer است یا نه، انجام شده است.

class GrayscaleImageTransformer : public GrayscaleTransformer {
public:
    cv::Mat transform_to_grayscale(cv::Mat image) override {
        cv::Mat grayscaleImage;
        cv::cvtColor(image, grayscaleImage, cv::COLOR_BGR2GRAY);
        return grayscaleImage;
    }
};

class RGBImageTransformer : public ImageTransformer {
public:
    cv::Mat transform_to_grayscale(cv::Mat image) override {
        cv::Mat grayscaleImage;
        cv::cvtColor(image, grayscaleImage, cv::COLOR_BGR2GRAY);
        return grayscaleImage;
    }
    
    cv::Mat transform_to_rgb(cv::Mat image) override {
        cv::Mat rgbImage;
        cv::cvtColor(image, rgbImage, cv::COLOR_GRAY2BGR);
        return rgbImage;
    }
};

int main() {
    GrayscaleImageTransformer grayscaleTransformer;
    RGBImageTransformer rgbTransformer;
    
    ImageProcessor grayscaleProcessor{ &grayscaleTransformer };

    cv::Mat image; 
    image = cv::imread("image.jpg");

    auto grayscaleResult = grayscaleProcessor.transform_to_grayscale(image);
    
    ImageProcessor rgbProcessor{ &rgbTransformer };
    auto rgbResult = rgbProcessor.transform_to_rgb(grayscaleResult);

    cv::imwrite("grey.jpg", grayscaleResult);
    cv::imwrite("rgb.jpg", rgbResult);

    return 0;
}

 

برای پیاده‌سازی DIP

DIP یا Dependency Inversion Principle (اصل وابستگی معکوس) یکی از اصول SOLID در شیء‌گرایی است که به مفهوم وابستگی به جایی یا Inversion of Control (IOC) هم معروف است. اصل DIP بیان می‌کند که برنامه باید به گونه‌ای طراحی شود که وابستگی به جزئیات پیاده‌سازی برنامه کاهش یابد و به جای آن، برنامه باید بر اساس واسط‌ها (interface) ارتباط برقرار کند، به گونه‌ای که خود وابستگی به جزئیات پیاده‌سازی به صورت بالادستی بر اساس واسط‌ها مدیریت شود.

با استفاده از اصل DIP، کد قابلیت بازگشت و تغییر بهتر را دارد. این بدان معناست که تغییر در پیاده‌سازی یک کد، تنها روی کد فعلی تأثیر نمی‌گذارد و به خاطر استفاده از واسط‌ها، به صورت گسترده‌تر در برنامه تأثیر می‌گذارد. با رعایت اصل DIP، کدها با استفاده از واسط‌ها باید طراحی شوند و نباید به کد پیاده‌سازی جزئیات برچسب تمایل یا وابستگی داشته باشند. به عبارت دیگر، برنامه باید بر اساس قرارداد کار کند نه کد پیاده‌سازی.

یک مثال ساده از پیاده‌سازی DIP در C++ به صورت زیر است:

فرض کنید یک برنامه ساده را برای محاسبه جدول ضرب در نظر بگیرید. برای پیاده‌سازی این برنامه، یک کلاس MatrixCalculator وجود دارد که دارای دو متد است که برای محاسبه جدول ضرب به کار می‌روند: multiply و print.

ابتدا یک واسط مشترک بین MatrixCalculator و کلاس‌های مختلف جدول تعریف می‌کنیم:

class IMatrix {
public:
    virtual void multiply() = 0;
    virtual void print() = 0;
};

سپس دو کلاس Matrix2x2 و Matrix3x3 را برای پیاده‌سازی واسط IMatrix تعریف می‌کنیم:

class Matrix2x2 : public IMatrix {
public:
    void multiply() {
        // implementation for 2x2 matrix multiplication
    }

    void print() {
        // implementation for 2x2 matrix printing
    }
};

class Matrix3x3 : public IMatrix {
public:
    void multiply() {
        // implementation for 3x3 matrix multiplication
    }

    void print() {
        // implementation for 3x3 matrix printing
    }
};

حالا کلاس MatrixCalculator را به گونه‌ای تغییر می‌دهیم که به جای استفاده از پیاده‌سازی خاص هر کلاس، از واسط IMatrix استفاده کند:

class MatrixCalculator {
private:
    IMatrix* matrix;
public:
    MatrixCalculator(IMatrix* m) : matrix(m) {}

    void multiply() {
        matrix->multiply();
    }

    void print() {
        matrix->print();
    }
};

بنابراین حالا می‌توانیم کلاس MatrixCalculator را به صورت زیر استفاده کنیم:

IMatrix* m2x2 = new Matrix2x2();
MatrixCalculator calculator2x2(m2x2);
calculator2x2.multiply();
calculator2x2.print();

IMatrix* m3x3 = new Matrix3x3();
MatrixCalculator calculator3x3(m3x3);
calculator3x3.multiply();
calculator3x3.print();

با این رویکرد، هر زمان که یک کلاس جدید با پیاده‌سازی واسط IMatrix ایجاد شود، می‌توانیم آن را به کلاس MatrixCalculator اضافه کرده و آن را بدون تغییر در کد MatrixCalculator استفاده کنیم. همچنین وابستگی MatrixCalculator به کلاس‌های Matrix2x2 و Matrix3x3 برای انجام کارش کاهش یافته است.

در C++20 می‌توان از ویژگی concepts برای پیاده‌سازی DIP استفاده کرد. در اینجا مثال ساده‌ای از پیاده‌سازی DIP با ویژگی concepts در C++20 آورده شده است:

#include <iostream>
#include <concepts>

template <typename T>
concept Comparable = requires(T a, T b) {
    { a == b } -> std::convertible_to<bool>;
    { a != b } -> std::convertible_to<bool>;
};

template <typename T>
class Calculator {
public:
    Calculator(T num1, T num2) : num1_(num1), num2_(num2) {}

    T add() requires Comparable<T> {
        return num1_ + num2_;
    }
    
private:
    T num1_;
    T num2_;
};

int main() {
    Calculator<int> intCalc(5, 10);
    std::cout << intCalc.add() << std::endl;

    Calculator<float> floatCalc(5.5, 10.5);
    std::cout << floatCalc.add() << std::endl;

    // Calculator<std::string> strCalc("hello", "world");   // Compile-time error
    return 0;
}

در این مثال، ما از ویژگی concepts برای تعریف واسط Comparable استفاده کرده‌ایم. همچنین، کلاس Calculator از واسط Comparable به عنوان یک شرط برای تعریف تابع add استفاده شده است. با این رویکرد، ما به سادگی می‌توانیم به جای وابستگی به یک نوع دقیق، وابستگی به یک واسط را ایجاد کنیم.

کامبیز اسدزاده

ساده‌ترین راه برای افزودن کد سفارشی به سایت‌هایی که بر پایهٔ وردپرس ساخته شده‌اند، بدون شکستن کد آن چیست؟

زبان برنامه‌نویسیِ ++C یکی از محبوب‌ترین زبان‌های برنامه‌نویسی است. آخرین آمار نشان می‌دهد که این زبان با سرعت بسیار زیادی در حال توسعه و پوست‌اندازی است.

cppwp.png

این زبان، علیرغم اینکه بیش از 40 سال از عمرش می‌گذرد، همچنان زبان انتخابی برای بسیاری از برنامه‌نویسان در سراسر جهان است. برای بسیاری از موارد مانند برنامه‌های کاربردی دستکاری تصویر، بازی‌های سه بعدی، شبیه سازی‌ها، مرورگرهای وب و نرم‌افزارهای سازمانی استفاده می‌شود.

دلیلش این است که سی‌پلاس‌پلاس در حال تکامل است، به انرژی سبز اهمیت می‌دهد و در عین حال کارآیی بسیار بالا و بهینه‌ای دارد. از آنجایی که این زبان پیچیده‌تر از سایر زبان‌های برنامه‌نویسی است، یک کمیتهٔ فرعی از یک سازمان چند سطحی وظیفه استانداردسازی آن را بر عهده دارد. سی‌پلاس‌پلاس اکنون از یک مدل قطار پیروی می‌کند و هر سه سال یکبار نسخه‌های جدید دریافت می‌کند که در مورد استاندارد‌های جدید، اخیراً مقالاتی منتشر شده است.

سی‌پلاس‌پلاس معمولاً برای توسعهٔ نرم‌افزار در مقیاس بزرگ استفاده می‌شود، اما می‌توان از آن برای پروژه‌های توسعه وب نیز استفاده کرد. ممکن است تعجب کنید که چگونه از آن با وردپرس، یکی از محبوب‌ترین سیستم‌های مدیریت محتوا و سازندگان وب سایت امروزه استفاده کنید.

در این باره قبلاً من مقالاتی را منتشر کرده‌ام که به صورت زیر آمده‌اند:

در حالی که بسیاری از وب سایت‌ها با استفاده از وردپرس به عنوان پایه ساخته می‌شوند، این لزوماً به این معنی نیست که اکوسیستم وردپرس تکامل یافته یا کامل است. به عنوان مثال، وردپرس تمرکز زیادی بر تجربه‌کاربران و نیازهای وبلاگ نویسان دارد، به همین دلیل است که بر چهار زبان HTML، CSS، جاوا اسکریپت و PHP متکی است و این بدان معناست که برای توسعه‌دهندگانی که می‌خواهند به طور کامل از قدرت آن استفاده کنند، محدودیت‌هایی وجود دارد. در حالی که افزونه‌هایی مانند Custom Post Types برای گسترش عملکرد وردپرس وجود دارد و راه‌های زیادی برای افزودن قابلیت‌های جدید بدون شروع از ابتدا وجود ندارد.

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

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

  1. کد ++C خود را بنویسید و آن را در یک برنامه یا کتابخانه مستقل کامپایل کنید.
  2. عملکرد برنامه یا کتابخانه ++C را از طریق وب API به نمایش بگذارید. شما می‌توانید از یک کتابخانه به عنوان cpprestsdk برای ایجاد نقاط پایانی API استفاده کنید.
  3. برنامه یا کتابخانه ++C را بر روی یک سرور، یا در همان سرور سایت وردپرس خود یا در یک سرور جداگانه، مستقر کنید.
  4. در سایت وردپرس خود، از یک افزونه یا کد سفارشی برای درخواست HTTP به API و بازیابی نتایج استفاده کنید. شما می‌توانید از کتابخانه‌ای مانند cURL برای ارائه چنین درخواست‌هایی استفاده کنید.
  5. در صورت نیاز از نتایج بازیابی شده در سایت وردپرس خود استفاده کنید.

راه دیگر برای افزودن قابلیت ++C به سایت وردپرس استفاده از افزونه‌ای است که به شما امکان می‌دهد کد ++C را مستقیماً در صفحات یا پست‌های وردپرس جاسازی کنید. مراحل بسته به افزونه‌ای که استفاده می‌کنید متفاوت است، اما در اینجا چند مرحله کلی وجود دارد که می‌تواند کمک کند.

  1. افزونه‌ای را که از کد ++C پشتیبانی می‌کند را در سایت وردپرس خود نصب کنید.
  2. یک صفحه یا پست جدید در وردپرس ایجاد کنید و کد کوتاه [cpp] را به قسمت محتوا اضافه کنید.
  3. در کد از نوع برچسب‌ِ [cpp]، کد ++C خود را اضافه کنید.
  4. صفحه یا پست را منتشر کنید و آن را مشاهده کنید تا کد جاسازی شده ++C را در عمل ببینید.

مجدداً توجه داشته باشید که افزودن کد ++C به طور مستقیم به یک صفحه یا پست می‌تواند خطرناک باشد. مهم است که پیاده‌سازی خود را به طور کامل آزمایش کنید تا مطمئن شوید که ایمن و قابل اعتماد است. از هر روشی که استفاده می‌کنید، به یک پایه محکم در سی‌پلاس‌پلاس و مهارت‌های یکپارچه سازی وردپرس نیاز دارد. وقتی صحبت از وردپرس به میان می‌آید، شاید بهتر باشد از زبانی مانند PHP استفاده کنید.

اگر می‌خواهید دربارهٔ ++C و نحوهٔ به حداکثر رساندن آن برای پروژه‌تان بیشتر بدانید، می‌توانید به آموزه‌های مربوط به سی‌پلاس‌پلاس در این سایت را بررسی کنید یا کتاب‌های پیشنهادی در بخش معرفی زبان را بخوانید. فراموش نکنید که سی‌پلاس‌پلاس یک زبان جذاب و همیشه در حال تکامل است و افزودن آن به مجموعه مهارت‌های شما سودمند است.

clightning
 

هنگامیکه شما برای اولین بار از C به CPP مهاجرت می کنید، یا اصلا برنامه نویسی را قصد دارید با CPP شروع کنید، با مفاهیم متعددی روبرو خواهید شد که شاید برای شما جالب باشند که بدانید، این ایده ها چطور شکل گرفتند، چطور به CPP افزوده شدند و اهمیت آن ها در عمل (هنگام برنامه نویسی و توسعه نرم افزار) چیست. در این پست وبلاگی IOStream، به این خواهیم پرداخت که ایده Overloading و Template و Auto Deduction چطور از CPP سر در آوردند. 

همانطور که شما ممکن است تجربه کرده باشید، هنگامیکه برنامه نویسی و توسعه نرم افزاری را با C شروع می کنید، برنامه شما چیزی بیش از یک مجموعه بی انتها از توابع و استراکچرها و متغیرها و اشاره گرها و ... نخواهند بود.

از همین روی شما مجبور هستید مبتنی بر ایده مهندسی نرم افزار و پارادیم برنامه نویسی ساخت یافته، برای هر کاری یک تابع منحصربفرد پیاده سازی کنید. این تابع باید از هر لحاظی از قبیل نام، نوع ورودی ها، نوع خروجی و حتی نوع عملکرد منحصربفرد باشد تا بتواند یک کار را به شکل صحیح کنترل کند که همین مسئله می تواند در پیاده سازی برخی نرم افزارها، انسان را در جهنم داغ و سوزان قرار بدهد. مثلا پیاده سازی یک برنامه محاسباتی مانند ماشین حساب که ممکن است با انواع داده های محاسباتی مانند عدد صحیح (Integer) و عدد اعشاری (Float) رو به رو شود. 

از همین روی فرض کنید، ما قرار است یک عمل محاسباتی مانند جمع از برنامه ماشین حساب را پیاده سازی کنیم. برای اینکه برنامه به شکل صحیحی کار کند، باید عمل جمع یا همان Add برای انواع داده های موجود از قبیل عدد صحیح و اعشاری پیاده سازی شود. اگر شما این کار را انجام ندهید، برنامه شما به شکل صحیحی کار نخواهد کرد (یعنی نتایج اشتباه ممکن است برای ما تولید کند). در تصویر زیر، نمونه این برنامه و توابع مرتبط با آن پیاده سازی شده است:

#include <stdio.h>

int AddInt(int arg_a, int arg_b)
{
	return arg_a + arg_b;
}

float AddFloat(float arg_a, float arg_b)
{
	return arg_a + arg_b;
}

double AddDouble(double arg_a, double arg_b)
{
	return arg_a + arg_b;
}

int main(int argc, const char* argv[])
{
	int result_int = AddInt(1, 2);
	float result_float = AddFloat(10.02f, 21.23f);
	double result_double = AddDouble(9.0, 24.3);

	printf("Result Integer: %d", result_int);
	printf("Result Float: %f", result_float);
	printf("Result Double: %lf", result_double);

	return 0;

}

به برنامه بالا دقت کنید. ما سه تا تابع Add با نام های منحصربفرد داریم که سه نوع داده مجزا را به عنوان ورودی دریافت می کنند، سه نوع نتیجه مجزا بازگشت می دهند، اگرچه پیاده سازی آن ها کاملا مشابه هم دیگر است و تفاوتی در پیاده سازی این سه تابع وجود ندارد.

ولی به هر صورت، اگر به خروجی دیزاسمبلی برنامه مشاهده کنید، دلیل این مسئله را متوجه خواهید شد که چرا هنگام برنامه نویسی با زبان C، به نام های منحصربفرد نیاز است، چون اگر توابع نام های مشابه با هم داشته باشند، لینکر نمی تواند به دلیل تداخل نام (Name Conflict)، آدرس آن ها را محاسبه یا اصطلاحا Resolve کند.

1_0SISKwE-kz5FeG0ujky69A.thumb.png.5873825ab0d6b941588a0289cc1a2d74.png

همانطور که در تصویر بالا خروجی دیزاسمبلی برنامه Add را مشاهده می کنید، اگر توابع نام مشابه داشتند، در هنگام فراخوانی (Call) تابع Add تداخل رخ می داد، چون دینامیک لودر سیستم عامل دقیقا نمی داند که کدام تابع را باید فراخوانی کند. برای همین نیاز است وقتی برنامه نوشته می شود، نام توابع در سطح کدهای اسمبلی و ماشین منحصر بفرد باشد.

به هر صورت، به نظر شما آیا راهی وجود دارد که ما پیاده سازی این نوع توابع را ساده تر کنیم یا حداقل بار نامگذاری آن ها را از روی دوش توسعه دهنده و برنامه نویس برداریم؟ بله امکان این کار وجود دارد. مهندسان CPP با افزودن ویژگی Overloading و Name Mangling یا همان بحث Decoration مشکل برنامه نویسان در پیاده سازی توابع با نام های منحصربفرد را حل کردند (البته کاربردهای دیگر هم دارد که فعلا برای بحث ما اهمیت ندارند).

ویژگی اورلودینگ در CPP به ما اجازه خواهد داد یک تابع با عنوان Add پیاده سازی کنیم که تفاوت آن ها فقط در نوع ورودی و نوع خروجی است. به عنوان مثال، در قسمت زیر، کد برنامه Add را مشاهده می کنید که با قواعد CPP بازنویسی شده است.

#include <iostream>

int Add(int arg_a, int arg_b)
{
	return arg_a + arg_b;
}

float Add(float arg_a, float arg_b)
{
	return arg_a + arg_b;
}

double Add(double arg_a, double arg_b)
{
	return arg_a + arg_b;
}

int main(int argc, const char* argv[])
{
	int result_int = Add(1, 2);
	float result_float = Add(10.02f, 21.23f);
	double result_double = Add(9.0, 24.3);

	std::cout << "Result Integer: " << result_int << std::endl;
	std::cout << "Result Float: " << result_float << std::endl;
	std::cout << "Result Double: " << result_double << std::endl;

	return 0;

}

همانطور که مشاهده می کنید، ما اکنون سه تابع با نام Add داریم. ولی شاید سوال پرسیده شود که چطور لینکر متوجه تفاوت این توابع با یکدیگر می شود درحالیکه هر سه دارای یک نام واحد هستند. اینجاست که مسئله Name Mangling یا همان Decoration نام آبجکت ها در CPP مطرح می شود. اگر شما برنامه مذکور را دیزاسمبل کنید، متوجه تفاوت کد منبع (Source-code) و کد ماشین/اسمبلی (Machine/Assembly-code) خواهید شد. 

1_3f8z67lwROt7c6uNFP7K9Q-min.jpg.ed9f3842e9816f346c10c5fb2a95c5e1.jpg

همانطور که در خروجی دیزاسمبلی برنامه اکنون مشاهده می کنید، توابع اگرچه در سطح کد منبع دارای نام مشابه با یکدیگر بودند، اما بعد کامپایل نام آن ها به شکل بالا تبدیل می شود. به این شیوه نام گذاری Name Mangling یا Decoration گویند که قواعد خاصی در هر کامپایلر برای آن وجود دارد.

این ویژگی موجب می شود در ادامه لینکر بتواند تمیز بین انواع توابع Add شود. به عنوان مثال، تابع نامگذاری شده با عنوان  j__?Add@YAHH@Z تابعی است که به نوعی از تابع Add اشاره دارد که ورودی هایی از نوع عدد صحیح دریافت می کند. این شیوه نامگذاری خلاصه موجب خواهد شد لینکر بتواند به سادگی بین توابع تمایز قائل شود.

با این حال هنوز یک مشکل باقی است، و آن هم تکرار مجدد یک پیاده سازی برای هر تابع است. به نظر شما آیا راهی وجود دارد که ما از پیاده سازی مجدد توابعی که ساختار مشابه برای انواع ورودی ها دارند، جلوگیری کنیم؟ باید بگوییم، بله. این امکان برای شما به عنوان توسعه دهنده CPP در نظر گرفته شده است.

ویژگی که اکنون به عنوان Templateها در مباحث Metaprogramming یا Generic Programming استفاده می شود، ایجاد شده است تا این مشکل را اساساً برای ما رفع کند. با استفاده از این ویژگی کافی است، طرح یا الگوی یک تابع را پیاده سازی کنید، تا در ادامه خود کامپایلر مبتنی بر ورودی هایی که به الگو عبور می دهید، در Backend، یک نمونه تابع Overload شده مبتنی بر آن الگو برای نوع داده شما ایجاد کند. 

#include <iostream>

template <typename Type>
Type Add(Type arg_a, Type arg_b)
{
	return arg_a + arg_b;
}

int main(int argc, const char* argv[])
{
	int result_int = Add(1, 2);
	float result_float = Add(10.02f, 21.23f);
	double result_double = Add(9.0, 24.3);

	std::cout << "Result Integer: " << result_int << std::endl;
	std::cout << "Result Float: " << result_float << std::endl;
	std::cout << "Result Double: " << result_double << std::endl;

	return 0;

}

به عنوان مثال، در بالا تابع Add را مشاهده می کنید که نوع داده ورودی این تابع و حتی نوع خروجی آن مشخص نشده است و در قالب Typename به کامپایلر معرفی شده است. این یک الگو برای تابع Add است. کامپایلر اکنون می تواند مبتنی بر ورودی هایی که به تابع هنگام فراخوانی یا اصطلاحا Initialization عبور می دهیم، یک نمونه تابع Overload شده از آن الگو ایجاد کند و در ادامه آن را برای استفاده در محیط Runtime فراخوانی کند. حال اگر برنامه بالا را دیزاسمبل کنید، مشاهده خواهید کرد که کامپایلر از همان قاعده Overloading استفاده کرده است تا نمونه ای از تابع Add متناسب با نوع ورودی هایش ایجاد کند.

1_KX5vFYWKZVDeGKgftQB1Cw.thumb.png.82cb68fab33fc3c52fb06ca58773eaae.png

هنوز می توان برنامه نویسی با CPP را جذاب تر و البته ساده تر کرد، اما چطور؟ همانطور که در قطعه کد بالا مشاهده می کنید، هنوز ما باید خود تشخیص دهیم که نوع خروجی تابع قرار است به چه شکل باشد. این مورد خیلی مواقع مشکل ساز خواهد بود.

برای حل این مسئله، در CPP مبحثی در نظر گرفته شده است که آن را به عنوان Auto Deduction می شناسیم که سطح هوشمندی کامپایلر CPP را بالاتر می برد. در این ویژگی خود کامپایلر است که مشخص می کند نوع یک متغیر مبتنی بر خروجی که به آن تخصیص داده می شود، چیست. به عنوان مثال، شما می توانید برنامه بالا را به شکل زیر بازنویسی کنید:

#include <iostream>

template <typename Type>
auto Add(Type arg_a, Type arg_b)
{
	return arg_a + arg_b;
}

int main(int argc, const char* argv[])
{
	auto result_int = Add(1, 2);
	auto result_float = Add(10.02f, 21.23f);
	auto result_double = Add(9.0, 24.3);

	std::cout << "Result Integer: " << result_int << std::endl;
	std::cout << "Result Float: " << result_float << std::endl;
	std::cout << "Result Double: " << result_double << std::endl;

	return 0;

}

با استفاده از ویژگی Auto Deduction و کلیدواژه Auto در برنامه، خود کامپایلر در ادامه مشخص خواهد کرد که تابع Add چه نوع خروجی دارد و همچنین نوع متغیرها برای ذخیره سازی خروجی Add چه باید باشد. به عبارتی اکنون تابع Add هم Value و هم Data type را مشخص می کند که این موجب می شود برنامه نویسی با CPP خیلی ساده تر از گذشته شود. حال اگر به نمونه برنامه آخر نگاه کنید و آن را با نمونه C مقایسه کنید، متوجه خواهید شد که CPP چقدر کار را برای ما ساده تر کرده است.

در این پست به هر صورت، قصد داشتم به شما نشان دهم که نحوه تحول CPP به صورت گام به گام چطور بوده است و البته اینکه پشت هر ویژگی در CPP چه منطق کلی وجود دارد. امیدوارم این مقاله برای شما مفید بوده باشد. نمونه انگلیسی این مقاله را می توانید در این آدرس (لینک) مطالعه کنید.

میلاد کهساری الهادی

کامبیز اسدزاده

معرفی نسخه‌بندی معنایی ویرایش ۲.۰

در دنیای مدیریت نرم‌افزار مکان مخوفی به نام «جهنم وابستگی» (dependency hell) وجود دارد. هر چه سیستم شما بزرگتر باشد و بسته‌های (package) بیشتری با نرم‌افزار شما یکپارچه شده باشند، احتمال بیشتری وجود دارد که روزی خود را دراین گودال ناامیدی بیابید.

در سیستم‌هایی با وابستگی‌های زیاد، انتشار بستهٔ جدید به زودی می‌تواند تبدیل به یک کابوس شود. اگر ویژگی‌های وابستگی‌ها بسیار جزئی‌نگرانه باشد، در خطر قفل نسخه (version lock) خواهید بود (ناتوانی برای بروزرسانی یک بسته، بدون اجبار جهت انتشار نسخه‌های جدید همهٔ بسته‌های وابسته). اگر وابستگی‌ها بسیار ضعیف مشخص شده باشند، به ناچار زخم بی‌قاعدگی نسخه را خواهید خورد (به فرض سازگاری بیش از حد معقول با نسخه‌های آتی‌تر). جهنم وابستگی آنجایی است که قفل نسخه و یا بی‌قاعدگی نسخه از پیشرفت رو به جلوی آسان و امن پروژهٔ شما جلوگیری می‌کند.

برای پاسخگویی به این مشکل، من یکسری قوانین و پیش‌نیازهای ساده را پیشنهاد میدهم که نحوهٔ تخصیص و افزایش شماره‌های نسخه را دیکته می‌کند. این قوانین برپایهٔ شیوه‌های موجود رایج و گستردهٔ در حال استفاده، هم در نرم‌افزارهای متن‌باز و غیر متن‌باز است، اگرچه لزوماً محدود به آن نیست. برای آنکه این سیستم کار کند نخست لازم است یک API عمومی (public) تعریف کنید. این امر ممکن است شامل مستندسازی، یا بوسیلهٔ خود کد مقید شده باشد. صرف نظر از این موضوع، مهم است که این API دقیق و واضح باشد. زمانیکه API عمومی خود را مشخص کردید، تغییرات آن را با افزایش معین شمارهٔ نسخهٔ خود مرتبط می‌سازید. قالب نسخهای به صورت X.Y.Z را در نظر بگیرید. خطاهایی که تاثیری بر API ندارند، نسخهٔ وصله (Patch) را افزایش می‌دهند، افزایش یا تغییر API که با نسخه‌های قبلی سازگار است، نسخهٔ جزئی (Minor) را افزایش می‌دهند، و تغییرات API که با نسخه‌های قبل ناسازگار هستند، نسخهٔ اصلی (Major) را افزایش می‌دهند.

SEMVER.png

این سیستم را «نسخه‌بندی معنایی» می‌نامیم. بر اساس این طرح، شماره‌های نسخه و روشی که تغییر می‌کنند، معنی و مفهومی را دربارهٔ کد تحت آن نسخه، و آنچه که از یک نسخه تا نسخه‌ای دیگر ویرایش شده است، انتقال می‌دهد.

به فرض اینکه نسخهٔ MAJOR.MINOR.PATCH یا اصلی.جزیی.وصله داده شده است:

  1. شمارهٔ نسخهٔ اصلی (MAJOR) را زمانی افزایش دهید که تغییرات API ناسازگار اعمال کرده‌اید،
  2. شمارهٔ نسخهٔ جزئی (MINOR) را زمانی افزایش دهید که قابلیت‌هایی اضافه کرده‌اید که با نسخه‌های قبل سازگار هستند،
  3. شمارهٔ نسخهٔ وصله (PATCH) را زمانی افزایش دهید که تصحیح خطاهایی (bug) اعمال کرده‌اید که با نسخه‌های قبل سازگار هستند.

برچسب‌های اضافی برای پیش‌نشر و ساختن فراداده به صورت پسوندهایی برای قالب MAJOR.MINOR.PATCH فراهم است.

ویژگی‌های نسخه‌بندی معنایی (SemVer)

کلمات کلیدی «باید»، «نباید»، «نیاز است»، «می‌بایست»، «نمی‌بایست»، «توصیه شده است»، «ممکن است» و «اختیاری» در این مستند می‌بایست بر مبنای آنچه در RFC 2119 تعریف شده است، معنا شوند.

  1. نرم‌افزارهایی که از نسخه‌بندی معنایی استفاده می‌کنند باید یک API عمومی اعلام کنند. این API می‌تواند در خود کد اعلام شود، یا به طور واضح در مستندسازی وجود داشته باشد. هر طور که انجام شود، می‌بایست دقیق و جامع باشد.

  2. یک شمارهٔ نسخهٔ عادی باید قالب X.Y.Z را داشته باشه طوری که در آن X ،Y و Z اعداد صحیح غیرمنفی هستند و نباید صفر اضافه (leading zero) داشته باشند. X نسخهٔ اصلی، Y نسخه جزیی، و Z نسخهٔ وصله است. هر عنصر باید به صورت شمارشی افزایش یابد. به عنوان مثال 1.9.0 -> 1.10.0 -> 1.11.0.

  3. زمانی‌که یک بستهٔ نسخه‌بندی شده منتشر شد، محتوای آن نسخه نباید دستکاری شود. هرگونه تغییری باید به عنوان نسخهٔ جدید منتشر شود.

  4. نسخهٔ اصلی شمارهٔ صفر (0.y.z) برای توسعه‌های ابتدایی است. هرچیزی در هر زمانی ممکن است تغییر کند. API عمومی نمی‌بایست ماندگار در نظر گرفته شود.

  5. نسخهٔ 1.0.0 API عمومی را تعریف می‌کند. روشی که شمارهٔ نسخهٔ بعد از این انتشار افزوده می‌شود، به این API عمومی و نحوهٔ تغییر آن وابسته است.

  6. نسخهٔ وصله Z (x.y.Z | x > 0) باید در صورتی افزوده شود که تصحیح‌های خطای سازگار با نسخهٔ قبلی معرفی شده باشند. یک تصحیح خطا به عنوان یک تغییر داخلی تعریف می‌شود که رفتارهای نادرست را اصلاح می‌کند.

  7. نسخهٔ جزیی Y (x.Y.z | x > 0) باید در صورتی افزوده شود که عملکرد سازگار با نسخه‌های قبل جدیدی به API عمومی معرفی شده باشد. همچنین اگر هرگونه عملکرد API عمومی به عنوان منسوخ‌شده برچسب خورده باشد، این شماره باید افزوده شود. اگر عملکرد جدید یا بهبود قابل توجهی در کدهای داخلی معرفی شده باشد، ممکن است نسخهٔ جزیی افزوده شود. ممکن است که شامل تغییرات سطح وصله هم باشد. زمانیکه نسخه جزیی افزوده شود، نسخهٔ وصله باید به 0 بازنشانده شود.

  8. نسخهٔ اصلی X (X.y.z | X > 0) باید در صورتی افزوده شود که هرگونه تغییرات ناسازگار با نسخه‌های قبل به API عمومی معرفی شده باشد. ممکن است این تغییرات شامل سطوح جزیی و وصله نیز باشد. نسخهٔ جزئی و وصله زمانیکه نسخهٔ اصلی افزوده می‌شود باید به 0 بازنشانی شوند.

  9. یک نسخهٔ پیش‌انتشار ممکن است با اضافه کردن یک خط تیره و یک سری شناسه‌هایی که به وسیلهٔ نقطه از هم جدا ‌شده‌اند و بلافاصله به دنبال نسخهٔ وصله می‌آیند، نشانه‌گذاری شود. شناسه‌ها باید تنها شامل اعداد و حروف الفبای اَسکی (ASCII) و خط تیره [A-Za-z0-9] باشند. شناسه‌ها نباید تهی باشند. شناسه‌های عددی نباید با صفر اضافه آغاز شوند. نسخهٔ پیش‌انتشار از اولویت پایین‌تری نسبت به نسخهٔ عادی مرتبط برخوردار است. یک نسخهٔ پیش‌انتشار حاکی از آن است که نسخهٔ ناپایدار است و ممکن است نیازمندی‌های سازگاری مورد نظر را آنگونه که در نسخهٔ عادی مرتبط نشان داده شده است، برآورده نکند. مثال: 1.0.0-alpha, 1.0.0-alpha.1, 1.0.0-0.3.7, 1.0.0-x.7.z.92.

  10. متادیتای ساخت (build metadata) ممکن است با افزودن یک علامت جمع (+) و یک سری شناسه‌هایی که به وسیلهٔ نقطه ازهم جدا شده‌اند، و بلافاصله به دنبال نسخهٔ وصله یا پیش‌انتشار می‌آیند، نشانه‌گذاری شود. شناسه‌ها باید تنها شامل اعداد و حروف الفبای اَسکی (ASCII) و خط تیره [A-Za-z0-9] باشند. شناسه‌ها نباید تهی باشند. متادیتای ساخت می‌بایست در زمان تعیین اولویت نسخه درنظر گرفته نشود. بنابراین دو نسخه که تنها در متادیتای ساخت با یکدیگر متفاوت هستند، اولویت یکسان دارند. مثال: 1.0.0-alpha+001, 1.0.0+20130313144700, 1.0.0-beta+exp.sha.5114f8

  11. اولویت اشاره دارد به اینکه چگونه نسخه‌ها زمانی‌که مرتب شده‌اند با یکدیگر مقایسه می‌شوند. اولویت باید به وسیلهٔ جداسازی نسخه به اصلی، جزیی، وصله و شناسه‌های پیش‌انتشار به همین ترتیب، محاسبه شود (متادیتای ساخت در اولویت نمایان نمی‌شود). اولویت، به وسیلهٔ اولین تفاوت تعیین می‌شود هنگامی که این مشخصه‌ها از چپ به راست مقایسه شوند، بدین صورت: نسخه‌های اصلی، جزیی و وصله همیشه به صورت عددی مقایسه می‌شوند. مثال: 1.0.0 < 2.0.0 < 2.1.0 < 2.1.1. زمانی که اصلی و جزیی و وصله برابر هستند، یک نسخهٔ پیش‌انتشار از اولویت کمتری نسبت به نسخهٔ عادی برخوردار است. مثال: 1.0.0- alpha < 1.0.0 اولویت برای دو نسخهٔ پیش‌انتشار با نسخهٔ اصلی، جزیی و وصلهٔ مشابه باید به وسیلهٔ مقایسهٔ هر مشخصه‌ای که با نقطه جدا شده، از چپ به راست مشخص شود تا زمانی که یک تفاوت به شرح زیر یافت شود: شناسه‌هایی که تنها شامل اعداد صحیح هستند به صورت عددی و شناسه‌هایی که با حروف یا خط‌های تیره به صورت الفبایی به ترتیب ASCII مقایسه می شوند. مشخصه‌های عددی همیشه از اولویت کمتری نسبت به مشخصه‌های غیرعددی برخوردار هستند. مجموعه‌های بزرگتری از بخشهای پیش‌انتشار اولویت بیشتری نسبت به مجموعه‌های کوچکتر دارند، اگر همه مشخصه‌های اولویت برابر باشند. مثال: 1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0-alpha.beta < 1.0.0-beta < 1.0.0-beta.2 < 1.0.0-beta.11 < 1.0.0-rc.1 < 1.0.0.

چرا از نسخه‌بندی معنایی استفاده شود؟

این ایده‌ای جدید یا انقلابی نیست. در واقع، احتمالاً شما چیزی مشابه به این را پیش از این انجام داده‌اید. مشکل اینجاست که «مشابه» به اندازهٔ کافی خوب نیست. بدون انطباق با نوعی تعریف رسمی، شماره‌های نسخه ضرورتاً برای مدیریت وابستگی (dependency) بلااستفاده هستند. بوسیلهٔ اختصاص اعداد و تعاریف واضح به ایده‌های بالا، برقراری ارتباط میان کاربران نرم‌افزار شما و اهدافتان آسان‌تر می‌شود. به‌مجرد اینکه این اهداف واضح شود، مشخصات وابستگی انعطاف‌پذیر (نه آن‌چنان انعطاف‌پذیر) می‌تواند نهایتاً ایجاد شود.

یک مثال ساده می‌تواند نشان دهد که چگونه نسخه‌بندی معنایی می‌تواند جهنم وابستگی را به خاطره‌ای از گذشته تبدیل کند. کتابخانه‌ای به نام «Firetruck» را در نظر بگیرید. این کتابخانه به یک بسته به نام «Ladder» که به صورت معنایی نسخه‌بندی شده، احتیاج دارد. در زمانی که firetruck ساخته شده، Ladder در نسخهٔ 3.1.0 است، شما می‌توانید وابستگی Ladder را با آسودگی به عنوان بزرگتر یا برابر با 3.1.0 و نه کمتر از 4.0.0 تعیین کنید. شما می‌توانید آن‌ها را در سیستم مدیریت بستهٔ خود منتشر کنید و بدانید که آن‌ها با نرم‌افزار وابسته موجود سازگار هستند.

بدون شک به عنوان یک توسعه‌دهندهٔ مسئولیت‌پذیر شما خواهید خواست که هر بسته همان‌طورکه اعلان شده ارتقاء یابد. دنیای واقعی مکان به هم ریخته ایست، ما جز اینکه هشیار باشیم نمی‌توانیم کاری دربارهٔ آن انجام دهیم. آنچه شما می‌توانید انجام دهید این است که بگذارید نسخه‌بندی معنایی به شما یک راه عاقلانه ارائه دهد، تا بسته‌ها را منتشر کرده و ارتقاء دهد بدون آنکه نسخه‌های جدیدی از بسته‌های مستقل را راه اندازی کند و شما را عذاب نداده، در وقت شما صرفه‌جویی کند..

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

 

سوالات متداول

چگونه باید با نسخه‌ها در فاز توسعهٔ ابتدایی 0.y.z کنار بیایم؟

ساده‌ترین کار برای انجام دادن این است که توسعهٔ ابتدایی خود را از انتشار 0.1.0 آغاز کنید و سپس نسخهٔ جزیی را برای هر انتشار آتی افزایش دهید.

چگونه بدانم چه زمانی باید 1.0.0 را منتشر کنم؟

اگر نرم‌افزار شما در طول تولید مورد استفاده قرار گرفته است، احتمالاً می‌بایست هم‌اکنون 1.0.0 باشد. اگر یک API ماندگار دارید که کاربران روی آن حساب می‌کنند، شما باید 1.0.0 باشید. اگر در مورد سازگاری با نسخه‌های قبل خیلی نگران هستید، احتمالاً می‌بایست هم‌اکنون 1.0.0 باشید.

آیا این روش، توسعه و تکرار سریع را کند نمی کند؟

نسخهٔ اصلی صفر تماماً در مورد توسعهٔ سریع است. اگر شما API را هر روز تغییر می‌دهید، یا باید هنوز در نسخهٔ 0.y.z باشید یا در یک شاخهٔ توسعهٔ جداگانه که بر نسخهٔ اصلی بعدی کار می‌کند هستید.

اگر حتی کوچکترین تغییرات ناسازگار با نسخه‌های قبل در API عمومی نیازمند یک نسخهٔ اصلی باشد، آیا من خیلی سریع به نسخه 42.0.0 نخواهم رسید؟

این سوال یک توسعه‌دهندهٔ مسئولیت‌پذیر و آینده‌نگر است. تغییرات ناسازگار نمی‌بایست به راحتی در نرم‌افزاری که کدهای وابستهٔ زیادی دارد معرفی شود. هزینه‌ای که برای ارتقاء باید متحمل شد می‌تواند قابل توجه باشد. اجبار برای ایجاد نسخه‌های اصلی برای انتشار تغییرات ناسازگار، یعنی شما به تأثیر تغییراتتان فکر خواهید کرد و نرخ هزینه/سود مورد نظر را خواهید سنجید.

مستندسازی تمامی API عمومی کار بسیار زیاد می‌برد!

این مسئولیت شما به عنوان یک توسعه‌دهندهٔ حرفه‌ای است تا به طور مناسب نرم‌افزار که می‌بایست توسط دیگران مورد استفاده قرار گیرد را مستندسازی کنید. مدیریت پیچیدگی نرم‌افزار یک بخش فوق‌العاده مهم ازکارآمد نگه‌داشتن پروژه است، و انجام آن سخت است اگر کسی نداند که چگونه نرم‌افزار شما را استفاده کند یا چه متدهایی برای صدا زدن امن است. در دراز مدت، نسخه‌بندی معنایی و پافشاری بر یک API عمومی خوش‌تعریف می‌تواند همه چیز و همه کس را در اجرا کردن راحت در موقعیت مناسبی نگه دارد.

چه کار می‌توانم بکنم اگر تصادفاً یک تغییر ناسازگار با نسخه‌های قبل را به عنوان نسخهٔ جزئی منتشر کردم؟

به مجرد اینکه متوجه این مورد بشوید، تنظیمات نسخه‌بندی معنایی را به هم زده‌اید، مشکل را حل کنید و یک نسخهٔ جزیی جدید که مشکل را تصحیح کند و سازگاری با نسخه‌های قبل را بازگرداند، منتشر سازید. حتی تحت این شرایط، این پذیرفته شده نیست که انتشارهای نسخه‌بندی شده را دستکاری کنید. اگر مناسب است نسخهٔ متخلف را مستند‌سازی کنید و کاربران خود را از مشکل مطلع سازید تا آن ها نیز از نسخهٔ متخلف آگاه باشند.

چه کار باید بکنم اگر وابستگی‌های خودم را بدون تغییر دادن API عمومی به‌روزرسانی کردم؟

این مورد تا زمانی که API عمومی را متأثر نسازد سازگار تلقی خواهد شد. نرم‌افزاری که صریحاً به همان وابستگی‌هایی که بستهٔ شما وابسته است، وابسته باشد، باید مشخصات وابستگی خود را داشته باشد و نویسنده باید هرگونه مغایرت را ذکر کند. تشخیص اینکه آیا تغییر ازنوع دستکاری در سطح جزیی است یا سطح وصله، به این بستگی دارد که آیا شما وابستگی‌های خود را جهت تصحیح یک خطا یا برای یک کاربرد جدید به‌روزرسانی کرده‌اید. من معمولاً کد اضافی برای موارد آتی در نظر می‌گیرم، که بدون شک این موارد افزایش سطح جزیی می‌باشد.

چه می شود اگر من بدون اعلام قبلی API عمومی را به صورتی تغییر دهم که با تغییر عدد نسخه سازگار نباشد؟ (یعنی کد به نادرست تغییر اصلی‌ای را در انتشار وصله معرفی می‌کند)

از بهترین قضاوت خود استفاده کنید. اگر شما مخاطبان زیادی دارید که به شدت به وسیلهٔ تغییر رفتار به آنچه قبلاً برای API عمومی در نظر گرفته شده، متأثر خواهند شد، پس بهترین کار انجام یک انتشار نسخهٔ اصلی است، حتی اگر اصلاح اعمال شده مؤکداً یک انتشار وصله محسوب شود. به یاد داشته باشید، نسخه‌بندی معنایی تماماً دربارهٔ انتقال معنا بوسیله چگونگی تغییر عدد نسخه می‌باشد. اگر این تغییرات برای کاربران شما مهم است، از عدد نسخه برای آگاه‌سازی آن‌ها استفاده کنید.

چگونه باید با منسوخ کردن عملکرد (deprecating functionality) کنار بیایم؟

منسوخ کردن عملکرد موجود بخشی عادی از توسعهٔ نرم‌افزار است و معمولاً برای این‌که پیشرفت رو به جلو حاصل شود مورد نیاز است. زمانی‌که بخشی از API عمومی خود را منسوخ می‌کنید، باید دو کار انجام دهید: ۱) مستندسازی خود را به‌روزرسانی کنید تا به کاربر اجازه دهید از تغییرات باخبر شود. ۲) یک انتشار جزیی که در آن قسمت منسوخ شده در جایگاه خود باشد منتشر کنید. قبل از آنکه عملکرد را به طورکامل در یک انتشار اصلی حذف کنید حتماً باید حداقل یک انتشار جزیی که شامل قسمت منسوخ شده است وجود داشته باشد تا کاربران به راحتی بتوانند به API جدید منتقل شوند.

آیا SemVer محدودیت اندازه بر روی رشتهٔ نسخه دارد؟

خیر، اما از قضاوت مناسبی استفاده کنید. به عنوان مثال یک نسخهٔ ۲۵۵ نویسه‌ای احتمالاً مفید نخواهد بود! همچنین، سیستم‌های خاص ممکن است محدودیت‌های خود برای اندازهٔ رشته اعمال کنند.

- منبع

قاسم رمضانی منش

توضیحات موردنیاز، قبلاً در این‌پیوند داده شده. حال بیاید ببینیم در عمل چگونه‌است ؟

Static Library یا کتابخانه‌های استاتیک :

        معمولاً تحت عنوان Archives هم شناخته می‌شوند، یک Static Library شامل مجموعه‌ای از Object-Fileها هست. Object-Fileها سورس‌های کامپایل‌ شدهٔ ما به زبان‌ماشین هستند. این فایل‌ها قابل اجرا نیستند چراکه هنوز کتابخانه‌های موردنیازشان Link نشده. برای کامپایل به‌صورت Object-File از فلگ -c استفاده می‌کنیم :

$> cc -c func.c
$> cc -c main.c
$> cc *.o -o output

در اینجا ما سورس‌کدهای func.c و main.c را فقط کامپایل کردیم و بعد (در خط سوّم) Object-Fileها را به کامپایلرمان دادیم تا عمل لینک کردن کتابخانه‌ها و خروجی‌نهایی را تولید کند. برای ساخت Static Library ما از Object-File‌ها به همراه برنامهٔ ar استفاده می‌کنیم، به این‌صورت که اوّل Object-Fileها را تولید می‌کنیم :

$> cc -c func1.c
$> cc -c func2.c

و حالا یک کتابخانه متشکل از Object-Fileها برای ساخت Static-Libraryمان خروجی می‌گیریم :

$> ar rcs libfunc.a func1.o func1.o

خب ! در این قسمت دو نکتهٔ کوچک و مهم وجود دارد :

  1. فایل‌هایی که با استفاده از فلگ -c کامپایل می‌کنید، خروجی‌حاصل فایلی با همان نام فایل ورودی به همراه پسوند .o می‌باشد.
  2. اسم کتابخانهٔ شما باید به‌صورت lib*.(a|os) باشد. و این‌چیزی هست که Linker به دنبال آن برای لینک‌کردن می‌گردد. برای کتابخانه‌های‌استاتیک ما از پسوند .a استفاده می‌کنیم و برای کتابخانه‌های‌داینامیک از .so .

حال برای استفاده از این کتابخانه‌ما نیاز به دوکار کوچک داریم هنگام کامپایل نهایی داریم :

$> cc main.c -L. -lfunc -o output
  1. فلگ -L برای مشخص کردن دایرکتوری‌ای که کتابخانهٔ ما در آن قرار دارد استفاده می‌شود. (میدانیم که در UNIX هر دایرکتوری دارای دو لینک می‌باشد؛ یک . (dot) که اشاره به دایرکتوری جاری دارد و.. (dot-dot) که اشاره به دایرکتوری-پدر (parent-directory یا دایرکتوری بالایی دارد).
  2. فلگ -l برای مشخص کردن اسم کتابخانهٔ ما استفاده می‌شود، دیدید که ما فقط اسم func را آوردیم، چرا که خود تصور می‌کند اوّل اسم فایل lib و پسوند آن .a یا .so می‌باشد.

یک نمونهٔ عملی را می‌توانید از این‌قسمت امتحان کنید :

2019-05-09_01-42.thumb.png.968d14c2943d21e8284b463f58b37cee.png

در این مثال از GNU Make استفاده شده است، درصورتی‌که آشنایی ندارید می‌توانید از این‌قسمت با GNU Make آشنا بشوید.

امّا نکته‌ای که قابل ذکر هست : در این‌جا شما فقط کتابخانه‌ای که خودتان نوشتید را به‌صورت Static لینک کردید، کتابخانه‌هایی مثل glibc به‌صورت خودکار درحالت Dynamic لینک می‌شوند.

 

Shared Library یا کتابخانه‌ داینامیک :

        در این روش بازهم ما نیاز به Object-Fileهای سورس‌کد‌ها داریم، با تفاوت اینکه باید فلگ -fPIC یا -fpic را اضافه کنیم که به معنی Position-independent Code می‌باشد؛ می‌دانید که Shared Libraryها یک‌بار فقط در حافظه بارگذاری می‌شوند از این رو نیاز است که سورس‌کدماشینی که تولید می‌شوند وابسته به این نباشد که در جای به خصوصی از حافظه بارگذاری شود. خب Object-Fileها را به صورت PIC کامپایل می‌کنیم :

$> cc -c -fPIC add.c
$> cc -c -fPIC sub.c

حال باید کتابخانهٔ‌اشتراکی خود را با استفاده از فلگ -shared ایجاد کنیم :

$> cc -shared add.o sub.o -o libmat.so

در اینجا ما از فلگ -shared استفاده کردیم و Object-Fileهای تولیدشده را به عنوان ورودی وارد کرده‌ایم. و حالا می‌توانیم از shared library خودمان استفاده کنیم :

$> cc main.c -o output -L. -lmath 

حال بیاید برنامه را اجرا کنیم :

$> ./output
./output: error while loading shared libraries: libmat.so: cannot open shared object file: No such file or directory

چرا ؟ به خاطر اینکه linker در آدرس‌های تعریف شده به دنبال کتابخانهٔ‌اشتراکی libmat.so می‌گردد. راه‌های مختلفی برای مشخص کردن مسیر کتابخانهٔ خودمان وجود دارد. 

  1. انتقال کتابخانهٔ خود به آدرس /usr/lib
  2. دستکاری LD_LIBRARY_PATH 
  3. ...

راه‌های مختلف را می‌توانید از لینک‌های زیر دنبال کنید :

  1. https://renenyffenegger.ch/notes/development/languages/C-C-plus-plus/GCC/create-libraries/index
  2. https://www.cprogramming.com/tutorial/shared-libraries-linux-gcc.html

موفق‌وپیروز باشید ?.

 

کامبیز اسدزاده

این چشم‌انداز احتمالاً برای دوست‌‌داران کتابخانهٔ قدرتمند Qt و طرفدارانش جذاب باشد! بنابراین من سعی کرده‌ام تا نتایج پست رسمی کیوت را در رابطه با چشم‌انداز فنی برای آیندهٔ کیوت نسخهٔ ۶ است در اختیار شما قرار دهم. تقریباً ۷ سال پیش کیوت نسخهٔ ۵.۰ منتشر شد! از آن زمان بسیاری از چیز‌ها در دنیای اطراف ما تغییر پیدا کرده است. و اکنون وقت آن رسیده است که چشم‌انداز جدیدی را از نسخهٔ جدید‌تر تعریف کنیم. بنابراین در این پست ما به معرفی مهمترین مواردی که به کیوت ۶ مرتبط است را می‌پردازیم.

qt6-vision.jpg

به نقل از مدیر فنی کیوت Lars Knoll، کیوت ۶ دقیقاً ادامهٔ کارهایی است که در نسخهٔ ۵ انجام داده شده است. بنابراین توسعه باید به گونه‌ای باشد که کاربران نباید اذیت شوند. اما نسخهٔ جدید می‌تواند یک آزادی بالاتر را در اجرای ویژگی‌های جدید، عملکرد و پشتیبانی بهتر از شرایط امروز و فردا از آنچه در حال حاضر می‌توان در سری ۵ داشته باشیم را به ما خواهد داد. همانطور که جزئیات بیشتر در زیر شرح داده شده است، کیوت ۶ هدف زیادی از سازگاری با نسخهٔ قبلی خود یعنی کیوت ۵ را خواهد داشت. همچنین ما در حال توسعه روی نسخهٔ ۵ نیز هستیم که قصد داریم برخی از ویژگی‌های کیوت ۶ را در نسخه‌های کیوت ۵.۱۴ و کیوت ۵.۱۵ LTS معرفی کنیم.

بنابراین با ثابت نگه‌داشتن ویژگی‌ها در کیوت ۵.۱۴، بیشترِ تمرکزِ تحقیق و توسعه به سمت کیوت ۶ تغییر خواهد یافت. بنابراین انتظار می‌رود کیوت ۶ تا پایان سال ۲۰۲۰ آماده شود. قبل از اینکه همه به چیز‌های جدید بپردازیم، بیایید برخی از ارزش‌های پایه از هستهٔ اصلی کیوت را برای کاربران خود یادآوری کنیم تا چیز‌هایی که نمی‌خواهیم تغییر کنند را تعریف کنیم.

چه چیزی Qt را برای کاربران ما ارزشمند می‌کند؟

کیوت محصولی است که در بازار‌های مختلفی مورد استفاده قرار می‌گیرد، ارزش‌های اصلی در هستهٔ کیوت برای مشتریان و کاربران ما عبارتند از:

  • ماهیت چند-سکویی آن، به کاربران این امکان را می‌دهد تا برنامه‌های خود را با استفاده از این فناوری به تمامی سیستم‌عامل‌های رو میزی، موبایل و سیستم‌های تعبیه شده (اِمبِد‌ها) مستقر کنند.
  • مقایس پذیری آن از دستگاه‌های کم مصرف و یک منظوره تا برنامه‌های دسکتاپ پیچیده و یا سیستم‌های متصل شده.
  • رابط‌های برنامه‌نویسی و ابزار‌ها و مستندات در سطح جهانی، ایجاد برنامه‌ها را ساده‌تر می‌کند.
  • حفظ، ثبات (پایداری) و سازگاری، امکان حفظ بانک بزرگی از کد‌ها با حداقل تلاش برای نگه‌داری آن‌ها.
  • یک اکو سیستم بزرگ توسعه‌دهنده با بیش از ۱ میلیون کاربر.

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

بسیاری از این دستگاه‌ها عملکردی نسبتاً ساده‌ای دارند، اما به رابط کاربری صیقلی و صافی نیاز دارند. بنابراین حجم زیادی از این دستگاه‌ها ایجاد می‌شود و ما باید اطمینان حاصل کنیم که می‌توانیم با ارائهٔ خود آن فضا را هدف قرار دهیم تا بتوانیم قوبل مقیاس پذیری خود را عملی کنیم.

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

عناصر هوش مصنوعی بیشتر در حوزهٔ برنامه‌ها و دستگاه‌ها مورد استفاده قرار می‌گیرد و ما نیاز به روش‌های آنسان برای ادغام با آن‌ها داریم.

رشد شدید تعداد دستگاه‌های متصل به هم و همچنین الزامات بسیار بالاتر در تجربه‌کاربر باعث می‌شود تا برای ساده سازی ایجاد برنامه‌ها و دستگاه‌ها، روی ابزار‌های کلاس جهانی تمرکز کنیم. هماهنگ سازی و ادغام طراحان UX در گردش کار توسعه یکی از اهداف است؛ اما بسیاری از زمینه‌های دیگر وجود خواهد داشت که ما باید برای ساده سازی زندگی کاربران تلاش کنیم.

کیوت ۶ یک نسخهٔ اصلی و جدید برای Qt خواهد بود؛ هدف اصلی با چنین نسخهٔ اصلی و جدید، آماده سازی کیوت برای شرایط مورد نیاز در سال ۲۰۲۰ و بعد از آن، تمیز کردن کد‌های پایهٔ ما و حفظ آسان‌تر است. به همین ترتیب تمرکز روی آن مواردی خواهد بود که نیاز به تغییرات معماری در کیوت دارند و بدون شکستن برخی از سازگاری‌ها با سری‌های کیوت ۵ قابل انجام نیست.

در زیر برخی از تغییرات اساسی که ما باید در کیوت ایجاد کنیم برای مناسب‌تر کردن آن برای سال‌های آینده ارائه شده است.

نسل بعدی کیو‌اِم‌اِل (QML)

زبان QML و فناوری Qt Quick فناوری‌های اصلی رشد سال‌های گذشتهٔ ما بوده است. روش‌های بصری ایجاد واسط‌های کاربری با استفاده از آن فناوری‌ها نقطه فروش بی نظیری از پیشنهاد ما است. اما QML، همانطور که برای کیوت ۵ ایجاد شده است، دارای تعداد زیادی تغییرات ناگهانی و محدودیت است. این به نوبهٔ خود به این معنا است که، امکان پیشرفت‌های چشم‌گیری وجود دارد که ما قصد داریم با کیوت ۶ آن‌ها را پیاده سازی کنیم.

معرفی وابستگی زیاد به نوع (strong typing)، وابستگی کم به نوع (weak typing) امکان ایجاد تغییر در کدها را برای کاربران ما سخت می‌کند. سیستمی از مدل وابستگی زیاد به نوع امکان پشتیبانی از این تغییرات را در محیط‌های یکپارچهٔ توسعهٔ نرم افزار و سایر ابزارها به کاربران می‌دهد و به طور چشمگیری حفظ و نگهداری از آن‌ها را راحت می‌کند. همچنین، قادر به تولید کدهای اجرایی هرچه بهتر و با سربار کمتر خواهیم بود.

اعمال JavaScript به عنوان یک ویژگی اختیاری، با توجه به این موضوع، داشتن یک موتور کامل جاوا اسکریپت هنگام استفاده از QML می‌تواند مشکلات را پیچیده‌تر کند و به خصوص هنگام هدف قرار دادن سخت‌افزار کم مصرف مانند میکرو کنترلرها یک مشکل اصلی محسوب می‌شود. اما در بسیاری از موارد استفاده از آن بسیار مفید است.

حذف نسخه سازی QML، با ساده کردن برخی از قوانین بررسی و جستجو و تغییرات در برخی از خواص می‌توانیم نیاز به نسخه را در QML حذف کنیم. این به نوبهٔ خود منجر به ساده سازی‌های زیاد در موتور کیو‌ام‌اِل می‌شود. حجم کار در حفظ فناوری کیوت کوئیک و ساده‌تر کردن استفاده از QML و Qt Quick را برای کاربران بسیار ساده‌تر خواهد کرد.

حذف ساختار داده‌های تکراری بین QObject و QML

در حال حاضر،  برخی از ساختار داده‌ها بین meta-object و QML  کپی و تکرار می‌شوند و عملکرد (کارایی و پرفرمنس) را در استارتاپ برنامه کاهش می‌دهد و باعث افزایش مصرف حافظه نیز می‌گردد. بنابراین با متحد کردن ساختار‌های داده‌ها، ما قادر خواهیم بود بخشی اعظمی از آن را حذف کنیم.

خودداری کردن از ساختار‌های داده تولید شده

این مربوط به نکتهٔ قبل است، جایی که در حال حاضر بسیاری از ساختار‌های داده تکراری در زمان اجرا تولید می‌شوند. باید تولید اکثر آن‌ها در زمان کامپایل کاملاً امکان‌پذیر باشد.

پشتیبانی از کامپایل QML برای بهره‌وری از کد‌های بومی C++، با وابستگی زیاد به نوع و قوانین جستجوی ساده‌تر، می‌توانیم QML را به کد‌های بومی C++ تبدیل کنیم که نتیجهٔ آن به طور قابل توجهی عملکرد زمان اجرا را افزایش می‌دهد.

پشتیبانی از پنهان کردن جزئیات اجرا، روش و خصوصیات «خصوصی» یک نیاز طولانی مدت است تا بتوانید داده‌ها و عملکرد‌ها را در اجزای QML پنهاد کنید.

هماهنگ‌سازی و ادغام بهتر ابزار‌ها، مدل کد‌های ما غالباً برای QML ناقص است و باعث می‌شود که تغییر مکان و خطاها را در زمان کامپایل غیر ممکن کند. با تغییرات فوق، می‌توان تشخیص کامپایلر را ارائه داد که بتواند با C++ و همچنین پشتیبانی از آن پالایشِ کد‌ها را بهبود بخشد.

نسل بعدی گرافیک‌ها

بسیاری از موارد در حوزهٔ گرافیک در نسخهٔ کیوت ۵ تغییر یافته‌اند. این باعث می‌شود که برای حفظ رقابت و توسعه در پُشته انجام شود. با کیوت ۵، ما از رابط‌های برنامه‌نویسی OpenGL را برای گرافیک‌های ۳ بعدی استفاده کردیم. از آن زمان به بعد، میزبانی از رابط‌‌های برنامه‌نویسی جدید نیز تعریف شده است. بنابراین وُلکان (Vulkan) جانشین مشخصی برای OpenGL در لینوکس است، اپل نیز مِتال (Metal) را تحت فشار قرار داد تا آن را جایگزین کند و مایکروسافت DirectX را دارد. این بدان معنی است که کیوت در آینده مجبور است به صورت یکپارچه با تمام رابط‌های برنامه‌نویسی کار کند. برای اینکه این ویژگی امکان‌پذیر باشد، باید یک لایهٔ جدید که رابط‌های برنامه‌نویسی گرافیکی را انتزاع می‌کند مانند (QPA برای ادغام سکو) به نام رابط سخت‌افزاری RHering تعریف شود. ما نیز باید زیر ساخت‌های ارائه شدهٔ خود (Qt Quick Scenegraph، QPainter و پشتیبانی ۳ بعدی) را در بالای آن لایه قرار دهیم.

مجموعهٔ رابط‌های برنامه‌نویسی گرافیکی مختلف باعث می‌شود که ما از زبان‌های مختلف سایه‌زنی پشتیبانی کنیم. ابزار Qt Shader به عنوان یک ماژول به ما کمک می‌کند تا سیستمِ سایه‌زنی را به صورت هم‌زمان (کراس‌-کامپایل) و در زمان اجرا کامپایل کنیم. بحث ۳ بعدی نقش مهم و مهمتری را ایفا می‌کند، و پشتیبانی فعلی ما یک راه حل یکپارچه برای ایجاد رابط کاربری (UI) ‌هایی که حاوی هر دو عنصر ۲ و ۳ بعدی باشد را ندارد. ادغام QML با محتوا از Qt3D و یا Qt 3D Studio در حال حاضر کار دشواری است و باعث سر‌ریز شدن برخی از کارایی‌ها و عناصر نمایشی می‌شود. علاوه بر این همگام سازی انیمیشن‌ها و انتقال‌ها بر روی یک فریم با سطح فریم بین محتوای ۲ و ۳ بعدی غیر ممکن است.

ادغام جدید محتوای ۳ بعدی با فناوری کیوت کوئیک با هدف حل این مشکل ایجاد شده است. در این حالت، یک سیستم ساخت (رندر) کامل و جدید به شما امکان می‌دهد تا محتوای ۲ و ۳ بعدی را با هم ظبط کنید. با این کار QML به زبان UI تعریف و تبدیل می‌شود که سه بعدی هستند و نیاز به فرمت UIP برطرف می‌شود. ما یک پیش‌نمایش از کیوت کوئیک جدید با پشتیبانی سه بعدی در حال حاضر با کیوت ۵.۱۴ ارائه می‌دهیم که اطلاعات بیشتر در یک پست جداگانه ارائه خواهد شد.

سرانجام پشتهٔ گرافیکی جدید نیاز به پشتیبانی از خط لولهٔ برای چیز‌های گرافیکی هستند که این امکان را می‌دهد تا آن‌هایی که در زمان کامپایل برای سخت افزار مورد نظر تهیه شده‌اند آماده کرده و از موارد مورد نظر استفاده کند. برای مثال، فایل‌های PNG را به بافت‌های فشرده تبدیل می‌کند و بسیاری از آن‌ها را به بافت (Texture) تبدیل کند. سایه‌ها و مِش‌ها را به قالب‌های باینری بهینه شده و موارد دیگر تبدیل خواهد کرد.

همچنین هدف ما این است که یک موتور متحد برای پوسته/ظاهر در کیوت ۶ ارائه دهیم که به ما این امکان را می‌دهد تا از نظر ظاهری و احساسات بر روی دسکتاپ و موبایل آن را بر روی هر دو فناوری کیوت‌ ویجت و کیوت‌ کوئیک ارائه کنیم.

ابزار یکپارچه و سازگار

ابزار‌های گرافیکی ما برای ساخت رابط‌های کاربری به دو بخش با استودیو کیوت ۳ بعدی (Qt 3D Studio) و استودیو طراحی کیوت (Qt Design Studio) تقسیم بندی شده‌اند. علاوه بر آن، استودیو ۳ بعدی اند;ی از بقیه کیوت جدا شده است که باعث می‌شود کمی بیشتر سعی بر آن شود!

ابزار‌های طراحی نیز به ایجاد محتوا مانند، محتوای ساخته شده در Photoshop، Sketch، Illustrator، Maya، 3DsMax و دیگر موارد ادغام شده است. ابزار‌های توسعه به توجه زیادی برای تمرکز دارد تا بتوانیم بهترین‌ها را در پشتیبانی کلاس برای QML، C++ و پایتون ارائه دهیم. یک ابزار متحد و یکپارچه این اجازه را می‌دهد که یک طراح UX بتواند از قابلیت‌های طراحی در کیوت کریتور استفاده کند و طراحان می‌توانند از ویژگی‌های ابزار‌های توسعه‌دهنده مانند تهیه یک پروژه یا آزمایش روی یک دستگاه بهره‌مند شوند.

ابزار ساخت QMake به عنوان ابزار ساخت در کیوت ۵ مورد استفاده قرار می‌گیرد که تعداد زیادی تغییرات ناگهانی و محدودیت‌ها خواهد دارد. برای کیوت ۶، هدف ما این است که CMake را به عنوان سیستم ساخت ثالث و استاندارد برای ساخت خود کیوت استفاده کنیم. چرا که سی‌میک تاکنون پرکاربرد‌ترین سیستم ساخت در جهان برای سی‌پلاس‌پلاس بوده است و ادغام هرچه‌بهتر آن کاملاً مورد نیاز است. البته پشتیبانی از QMake ادامه خواهد داشت، اما آن را توسعه نخواهیم داد یا از آن برای ساخت فریم‌ورک کیوت استفاده نخواهیم کرد.

بهبود رابط‌های برنامه‌نویسیC++

سی‌پلاس‌پلاس طی سال‌های گذشته تغییرات بسیار زیادی کرده است؛ در حالی که ما مجبور بودیم کیوت ۵.۰ را روی سی‌پلاس‌پلاس ۹۸ پایه‌گذاری کنیم. اما اکنون می‌توانیم به سی‌پلاس‌پلاس ۱۷ برای پایه‌گذاری کیوت ۶ اطمینان کنیم. این بدان معنی است که C++ عملکرد بسیار بیشتری را نسبت به زمان توسعه و اجرای کیوت ۵ که در دسترس نبود ارائه خواهد کرد. هدف ما با کیوت ۶ بهتر شدن با یکپارچه‌سازی و ادغام قابلیت‌ها  بدون از دست دادن پشتیبانی و سازگاری از روش‌های پیشین (رو به عقب یا همان backward compatibility) است.

برای کیوت ۶، هدف ما این است که برخی از قابلیت‌های معرفی شده با QML و فناوری Qt Quick را از طرف C++ در دسترس قرار دهیم. بنابراین ما در تلاش برای معرفی یک سیستم خاص برای QObject و کلاس‌های مرتبط هستیم. موتور اتصال دهنده را از QML در هستهٔ کیوت ادغام می‌کنیم و آن را از سی‌پلاس‌پلاس در دسترس قرار می‌دهیم. این سیستم خاص از موتور اتصال به کاهش قابل توجهی در سربار زمان کار و مصرفه حافظه در اتصال منجر می‌شود و آن‌ها را برای همهٔ قسمت‌های Qt، نه تنها Qt Quick قابل دسترس می‌کند.

پشتیبانی از زبان

با کیوت ۵.۱۲، پشتیبانی از پایتون معرفی شده است. همچنین مرورگر را به عنوان پلتفرم جدید از طریق کیوت برای وِب اسمبلی اضافه کرده‌ایم. پس از انتشار کیوت ۶.۰ نگه‌داشتن و گسترش بیشتر بر روی سطح چند‌-سکویی بخش مهمی از اهداف و مسیر توسعهٔ سری‌های کیوت ۶ خواهد بود.

سازگاری با کیوت ۵ و افزایش سازگاری‌ها و بهبود‌ها

سازگاری با نسخه‌های قدیمی‌تر بسیار مهم است، بنابراین وقتی کیوت ۶ را توسعه می‌دهیم یک نیاز اساسی محسوب می‌شود. توسط چهارچوب کیوت میلیون‌ها خط کد نوشته شده است و هرگونه تغییرات در ناسازگاری که انجام شود هزینه‌ای را برای کاربران خواهد داشت. علاوه‌ بر این، کار بیشتری برای تغییرات در کیوت ۶ نیاز است تا کاربران کم کم با آن سازگار شوند که منجر به هزینه‌های بیشتر از طرف تیم توسعهٔ کیوت برای حفظ آخرین نسخه کیوت ۵ خواهد بود.

به این ترتیب، ما باید به فکر جلوگیری از ساطع شدن خطاهای احتمالی در زمان کامپایل و یا زمان اجرا برای کاربران می‌شود باشیم. در حالی که ما نیاز به حذف بخش‌هایی از کیوت خواهیم داشت، باید اطمینان حاصل کنیم که کاربران ما از عملکرد مورد نیاز خود برخوردار هستند. این بدان معنا است که کلید‌هایی مانند Qt Widgets و سایر قسمت‌هایی که توسط بخش بزرگی از کاربران ما مورد استفاده قرار می‌گیرد، در دسترس باشد.

ما در حال برنامه‌ریزی برای افزایش بسیاری از پیشرفت‌ها در کلاس‌های اصلی و عملکردی هستیم که در سری کیوت ۵ نتوانستیم انجام دهیم. هدف این است که سازگاری کامل منبع را حفظ کنیم، اما از آنجا که می‌توانیم سازگاری باینری را با کیوت ۶ بشکنیم، می‌توانیم پاک‌سازی‌ها و اصطلاحات کاملاً زیادی را انجام دهیم که در کیوت ۵ نمی‌توانستیم آن را عملی کنیم.

با این وجود، ما باید به جلو پیش برویم و برخی از پاک‌سازی‌ها که در کیوت ۵ در مورد کلاس‌ها، توابع و یا ماژول‌ها عنوان شده بود را در کیوت ۶ به طور کامل اعمال کنیم. این کار باعث می‌شود ما روی مبنای کد‌گذاری شدهٔ فعلی تمرکز بیشتر و بهتری داشته باشیم.

با این حال، انتقال به دور از قسمت‌های منسوخ شده باید تا حد امکان ساده باشد و کاربران ما می‌توانند با استفاده از کیوت ۵.۱۵ «پشتیبانی بلند مدت» به صورت ایده‌آل این کار را انجام دهند. هدف ما باید این باشد که کیوت ۶ به اندازهٔ کافی با نسخهٔ ۵.۱۵ سازگار باشد تا فرد بتواند به راحتی یک بخش اعظمی از کد خود را حفظ کند، به طوری که کد آن در هر دو نسخهٔ ۵ و ۶ قابل کامپایل باشد.

بازار و ساختار فنی محصول

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

کیوت طی سال‌های گذشته رشد بسیار زیادی داشته است، تا جایی که ارائهٔ نسخهٔ جدید آن یک کار مهم است. با استفاده از کیوت ۶ فرصتی برای باز‌سازی محصولات ارائه شده ما وجود دارد و یک محصول اصلی و کوچکتر که شامل چهارچوب‌ها و ابزار‌های اساسی است. ما از بازار استفاده خواهیم کرد تا چهارچوب و ابزار‌های اضافی خود را ارائه دهیم، نه به عنوان یک بسته‌نرم‌افزاری وابسته به کیوت!

چشم‌انداز فنی تا اولین نسخهٔ کیوت ۶ تکامی خواهد یافت. اگرچه معتقد هستیم که این سند بسیاری از مهمترین نکات را برای نسخهٔ بعدی کیوت معرفی می‌کند اما مطمئناً کامل نیست. اگر شما هم ایدهٔ دیگری دارید می‌توانید آن را با ما در میان بگذارید.

Max Base

سلام وقت بخیر,

گیت هاب پیج

گیتهاب پیج یک سرویس میزبانی وب است که توسط Github اراعه شده است که با استفاده از آن می توانید حتی صفحاتی Static را در پلتفرم وب میزبانی کنید و حتی ابزار هایی مانند Jekyll را می توانید روی این بستر (Github Pages) پیاده و اجرا کنید که در قسمت های آینده در مورد آن توضیح خواهم داد.

میزبانی وب سایت هایی که بر پایه Github Pages هستند بصورت رایگان هست و دیگر نیازی به دامین, هاست و سرور وجود ندارد.

حساب های گیت هاب

تمام حساب هایی که در سایت گیت هاب وجود دارند دو نوع هستند.

  1. حساب کاربری و شخصی
  2. حساب سازمانی (تیم)

بطور مثال :

حساب هایی مانند:

همگی حساب های فرد و شخصی هستند.

و حساب هایی مانند :

  1. Microsoft · GitHub,
  2.  Google · GitHub,
  3.  TeamSnap · GitHub,
  4.  iOSTREAM · GitHub 

.حساب هایی سازمانی (تیم) هستند

هر حساب کاربری و شخصی این قابلیت را دارد تا بتواند یک حساب سازمانی ایجاد کند.

ایجاد حساب سازمانی (تیم)

برای ایجاد کردن یک حساب سازمانی, بعد از عضویت و ایجاد یک حساب کاربری در Github و وارد شدن به حساب کاربریتان, به اینجا مراجعه کنید تا یک سازمان جدیدی ایجاد کنید.

20571156026532415692_Screenshot%20from%2

 

گیت هاب پیج

به موضوع اصلی برمیگردیم. تا کنون در مورد انواع حساب ها توضیح داده ایم. حال در نظر داشته باشید که هر حساب کاربری (عادی یا سازمانی) این قابلیت را دارد تا بتواند یک میزبانی برای خودش رزرو کند.

در حالت پیشفرض میزبانی ها بصورت رایگان بر روی دامین GitHub Pages قرار دارند، با اینحال بطور مثال اگر شما یک حساب (شخصی یا سازمانی) بنام test456 دارید می‌توانید میزبانی خود را بر روی دامین http://test456.github.io فعال کنید.

آزمایشگاه و انجام بصورت عملی

در حال حاظر من خودم یک سازمان (تیم) بنام MaxFork با آدرس Max Fork · GitHub در اختیار دارم.

می خواهم یک میزبانی وب جدید روی این حساب ایجاد کنم.

20874156026574223837_Screenshot%20from%2

گزینه New را لمس کنید و یک مخزن جدید (پروژه) ایجاد کنید.

دقت کنید که نام پروژه اهمیت دارد را آدرس میزبانی که مد نظرمان هست وارد می کنیم.

بطور مثال MaxFork.Github.io

محتوای Description اختیاری است و می توانید هر چه میخواهید وارد کنید. چون بعدا هم امکان ویرایش کردن آن برایتان وجود ندارد.

نوع پروژه را می توانید Public یا Private تنظیم کنید. با توجه به اینکه حساب های اکثر افراد معمولی هست گمان میکنم امکان تنظیم کردن از نوع Private برای آنها همراه با میزبانی وب وجود نداشته باشد. بنابراین Public را انتخاب کنید. 

پیاده کردن یک فایل README برای مخزن اختیاری است و می توانید آنرا ایجاد کنید.

همچنین تصمیم شما در مورد انتخاب لایسنس نیز آزادانه است و می توانید خودتان شخصا در مورد آن فکر و تصمیم بگیرید.

6076156026619835898_Screenshot%20from%20

برای امتحان می توانید فایلی بنام index.html را با یک محتوای آزمایشی ایجاد کنید.

در حال حاظر وب سایتی با آدرس https://maxfork.github.io در دسترس وجود دارد و محتوایی را که در فایل نوشته ایم را نشان می دهد.

 

در انتهای بخش Settings در قسمت GitHub Pages می توانید وضعیت میزبانی را بررسی کنید.

در قسمت ذکر شده می توانید قالب های از قبل تعریف شده را مشاهده و انتخاب کنید.

همچنین امکان تنظیم Custom domain برای میزبانی را هم دارید.

 

همچنین می توانید فایل های دیگری نیز مانند test.html همراه با پوشه و مسیردهی ایجاد کنید.

بطور مثال اکنون ما فایل new.html را در شاخه اصلی مخزن ایجاد کردیم که در آدرس زیر قابل دسترس است:

https://maxfork.github.io/new.html

فایلی بنام _config.yml از قبل تعریف شده است که می توانید بصورت دستی هم ایجاد کنید که در آن نام قالب / تنظیمات / پلاگین و ماژول هایی را که نیاز دارید می توانید تعریف کنید.

در قسمت های بعدی در مورد Static بیشتر توضیح خواهم داد، پیشنهاد می‌کنم در مورد Textile و Markdown هم مطالعه کنید.

 

سپاس

Max Base / مکس بیس

Max Base

با سلام وقت بخیر,

در این مطلب میخواهیم در مورد روش کارکرد پیام رسان ها بیشتر بدانیم و با یکدیگر کد یک پیام رسان ساده را پیاده و بررسی کنیم. 

1200px-Client-server-model.svg.png

طرز کار کرد پیام رسان

در نظر داشته باشید که هر پیام رسانی که بر ساختار ها پیاده شده باشد از دو قسمت تشکیل شده است.

  1. نرم افزار اصلی برای مدیریت درخواست ها (سرور)
  2. نرم افزار برای کاربران (کاربر)

به نرم افزار اولی سمت SERVER خواهیم گفت و به بعدی سمت CLIENT خواهیم گفت.

روم یا تالار گفتگو

ما تنها یک اتاق برای گفتگو در نظر میگیریم و هر کاربری که به سرور متصل شود را در همان تالار اضافه خواهیم کرد.

تالار های گفتگو صرفا برای تقکیک سازی ارسال و دریافت ها و محدود کردن بازه ی کاربران مورد نظر... (ممکن است یک کاربر در چند اتاق بطور همزمان باشد.)

سیستم های پیام رسان پیشرفته تر مانند تلگرام و ... تالار های زیادی را شامل می شوند. (هر کاربر خودش در کانال و گروه های مختلفی عضو است که هر کدام از آنها یک کانال متفاوت محسوب می شوند.)

* دقت شود که منظور از کانال صرفا یک اتاق یا تالار گفتگو است و منظور کانال ارتباطی و پروتکل نیست.

نرم افزار اصلی

نرم افزار اصلی وظیفه دارد تا تمام کاربرانی که وارد تالار شده اند را به یاد داشته باشد و هر لحظه اماده دریافت درخواست هایی از طرف کاربرانش باشد. و پیام هایی را که از کاربران دریافت می کند برای تمامی کاربران دیگر هم ارسال کند که بسته به خلاقیت و نیاز می تواند هر یک از این بخش ها متفاوت طراحی شود.

نرم افزار اصلی باید از قبل اجرا شده باشد. تا کاربران دیگر با استفاده از نرم افزار مخصوص به خودشان بتوانند به سرور متصل شوند و ارسال و دریافت داشته باشند.

در نظر داشته باشید که اگر در نرم افزار اصلی اختلالی پیش بیایید و متوقف بشوند. قطا برای تمام کاربران مشکل پیش می آید. مگر اینکه از پایگاه های داده ی داخلی استفاده کرده باشند. (با خلاقیت می توان به گونه های متفاوتی چنین ساختاری را پیاده کرد)

نرم افزار کاربران

این نرم افزار جذاب ترین بخش است چرا تمام قابلیت هایی را که در اختیار کاربر قرار می دهیم را مستقیما طراحی می کنیم.

در نظر داشته باشید که هر چیزی که در این نرم افزار طراحی می شود باید در نرم افزار اصلی پشتیبانی شوند... بنابراین اگر این دو بخش توسط دو فرد یا دو گروه مجزا طراحی می شوند آنها باید توسط داکیومنت ها و جلساتی به نظرات مشابه ای رسیده باشند. (اگرچه اینها تخصص و وظیفه ی تحلیلگر سیستم است! نه بطور همزمان وظیفه توسعه دهنده و برنامه نویس نرم افزار)

پیاده سازی یک نمونه

اکنون در نظر داریم تا با استفاده از ساختار کتابخانه BoostAsio پروژه ای را با نام BoostAsioChat ایجاد کنیم که در آن می خواهیم یک پیام رسان با حداقل ترین امکانات پایه طراحی کنیم که بیشتر جنبه شخصی و تفریحی دارد. زیرا از ساختار های استاندارد و ایمن و کاربری کاملا بدور است! (می توانید خودتان توسعه دهید و آنرا جالب تر بسازید)

 

ساختار نرم افزار اصلی و سرور را به این صورت تعریف می کنیم :

typedef deque<message> messageQueue;

class participant {
    public:
        virtual ~participant() {}
        virtual void deliver(const message& messageItem) = 0;
};

typedef shared_ptr<participant> participantPointer;

class room {
    public:
        void join(participantPointer participant);
        void deliver(const message& messageItem);
        void leave(participantPointer participant);
    private:
        messageQueue messageRecents;
        enum { max = 200 };
        set<participantPointer> participants;
};

class session : public participant, public enable_shared_from_this<session> {
    public:
        session(tcp::socket socket, room& room) : socket(move(socket)), room_(room);
        void start();
        void deliver(const message& messageItem);
    private:
        void readHeader();
        void readBody();
        void write();
        tcp::socket socket;
        room& room_;
        message messageItem;
        messageQueue Messages;
};

class server {
    public:
        server(boost::asio::io_context& io_context, const tcp::endpoint& endpoint) : acceptor(io_context, endpoint);
    private:
        void do_accept();
        tcp::acceptor acceptor;
        room room_;
};

int main(int argc, char* argv[]);

ساختار نرم افزار کاربر را هم به این صورت تعریف می کنیم :

typedef deque<message> messageQueue;

class client {
    public:
        client(boost::asio::io_context& context, const tcp::resolver::results_type& endpoints) : context(context), socket(context);
        void write(const message& messageItem);
        void close();
    private:
        void connect(const tcp::resolver::results_type& endpoints);
        void readHeader();
        void readBody();
        void write();
        boost::asio::io_context& context;
        tcp::socket socket;
        message readMessage;
        messageQueue writeMessage;
};

int main(int argc, char* argv[]);

 

در نظر داریم تا در این پروژه از thread ها نیز استفاده کنیم... در مورد این مفهوم ها می توانید بصورت مجزا تحقیق کنید.

بنابراین روش کامپایل این پروژه به این صورت خواهد بود :

 

$ g++ client.cpp -lpthread -o client
$ g++ Server.cpp -lpthread -o server

 

آزمایش

همانطور که گفته شد در ابتدا نرم افزار اصلی و سرور باید اجرا شود.

در اینجا ما تمام ارتباطات شبکه را بر روی یک سیستم در شبکه داخلی برقرار خواهیم کرد... پس نگرانی در مورد ساختار های درونی شبکه و آی پی / دی ان اس / دامین نخواهیم داشت.

بنابراین ای پی را می توانید ای پی داخلی یا  localhost  در نظر بگیرید.

برای آزمایش پورت فرضی 4000 را در نظر میگیریم و نرم افزار اصلی را روی این پورت اجرا میکنیم :

 

$ ./server 4000

در این مرحله متوجه می شوید که نرم افزار اصلی با موفقیت اجرا شده است و همچنان اجرا مانده است. بله درست است... نرم افزار اصلی هر لحظه باید منتظر دستور کاربران باشد.

اگر لحظه ای برای نرم افزار اصلی اختلالی پیش آید نخواهد توانست دستورات کاربران را انجام یا پاسخ دهد.

بنابراین این پردازش را قطع نکنید و اجازه دهید تا نرم افزار اصلی اجرا بماند. در محیط دیگری نرم افزار سمت کاربر را نیز اجرا کنید.

این نرم افزار را می توانید به تعداد دلخواه وارد کنید. (همانطور که ممکن است 6 نفر همزمان به سرور متصل باشند / یا ممکن است هیچ فردی به سرور متصل نشوند)

 

ابتدا یک کاربری را به سرور با پورت 4000 و شبکه داخلی وصل می کنیم :

 

$ ./client localhost 4000
first user: you can type message here...

حال در محیط دیگری با کاربر جدیدی نیز وارد می شویم :

$ ./client localhost 4000
second user: you can type message here...

در این پروژه نمونه از کاربران نام کاربری یا نام نمی پرسیم.. و صرفا وقتی وارد محیط گفتگو می شوند... یا زمانی که به سرور متصل می شوند منتظر هستیم تا انها پیامی را بنویسند...

هر پیامی را که بنویسند به سرور ارسال می شود و سرور وظیفه دارد تا آنرا برای تمام کاربران بفرستد. و این روند درون یک حلقه بی نهایت تکرار می شوند.

پس این ارتباط دو طرفه خواهد بود و هم کاربران برای سرور اطلاعات ارسال می کنند و هم سرور برای کاربران اطلاعات ارسال خواهد کرد.

در نظر داشته باشید که کاربر اول می تواند پیامی را بنویسد و به کاربران دیگر ارسال شود. ممکن است کاربر سومی اصلا تصمیمی به نوشتن پیام نداشته باشد و صرفا تمایل به خواندن پیام دیگران داشته باشند. و این کاملا اختیاری است. و ما کاربران را اجباری نمیکنیم. اگرچه شما می توانید با خلاقیت خودتان اینها را با متغییر های کمکی و دستورات شرطی پیاده کنید.

کد ها

برای پیام ها یک ساختار در نظر میگیریم و بصورت مشترک در هر دو نرم افزار استفاده خواهیم کرد... بنابراین اینرا در هدر پیاده خواهیم کرد.

هدر پیام : (message.hpp)


#ifndef message_HPP
    #define message_HPP

    #include <cstdio>
    #include <cstdlib>
    #include <cstring>
    using namespace std;
    class message {
        public:
            enum { headerLength = 4 };
            enum { maxBodyLength = 512 };
            message() : bodyLength_(0) {
            }
            const char* data() const {
                return data_;
            }
            char* data() {
                return data_;
            }
            size_t length() const {
                return headerLength + bodyLength_;
            }
            const char* body() const {
                return data_ + headerLength;
            }
            char* body() {
                return data_ + headerLength;
            }
            size_t bodyLength() const {
                return bodyLength_;
            }
            void bodyLength(size_t new_length) {
                bodyLength_ = new_length;
                if(bodyLength_ > maxBodyLength)
                    bodyLength_ = maxBodyLength;
            }
            bool decodeHeader() {
                char header[headerLength + 1] = "";
                strncat(header, data_, headerLength);
                bodyLength_ = atoi(header);
                if(bodyLength_ > maxBodyLength) {
                    bodyLength_ = 0;
                    return false;
                }
                return true;
            }
            void encodeHeader() {
                char header[headerLength + 1] = "";
                sprintf(header, "%4d", static_cast<int>(bodyLength_));
                memcpy(data_, header, headerLength);
            }
        private:
            char data_[headerLength + maxBodyLength];
            size_t bodyLength_;
    };

#endif

نرم افزار اصلی و سرور : (server.cpp)

#include <iostream>
#include <cstdlib>
#include <deque>
#include <memory>
#include <list>
#include <set>
#include <utility>
#include <boost/asio.hpp>
#include "message.hpp"
using boost::asio::ip::tcp;
using namespace std;
typedef deque<message> messageQueue;
class participant {
    public:
        virtual ~participant() {}
        virtual void deliver(const message& messageItem) = 0;
};
typedef shared_ptr<participant> participantPointer;
class room {
    public:
        void join(participantPointer participant) {
            participants.insert(participant);
            for(auto messageItem: messageRecents)
                participant->deliver(messageItem);
        }
        void deliver(const message& messageItem) {
            messageRecents.push_back(messageItem);
            while(messageRecents.size() > max)
                messageRecents.pop_front();
            for(auto participant: participants)
                participant->deliver(messageItem);
        }
        void leave(participantPointer participant) {
            participants.erase(participant);
        }
    private:
        messageQueue messageRecents;
        enum { max = 200 };
        set<participantPointer> participants;
};
class session : public participant, public enable_shared_from_this<session> {
    public:
        session(tcp::socket socket, room& room) : socket(move(socket)), room_(room) {
        }
        void start() {
            room_.join(shared_from_this());
            readHeader();
        }
        void deliver(const message& messageItem) {
            bool write_in_progress = !Messages.empty();
            Messages.push_back(messageItem);
            if(!write_in_progress)
            {
            write();
            }
        }
    private:
        void readHeader() {
            auto self(shared_from_this());
            boost::asio::async_read(socket,
            boost::asio::buffer(messageItem.data(), message::headerLength), [this, self](boost::system::error_code ec, size_t) {
                if(!ec && messageItem.decodeHeader()) {
                    readBody();
                }
                else {
                    room_.leave(shared_from_this());
                }
            });
        }
        void readBody() {
            auto self(shared_from_this());
            boost::asio::async_read(socket, boost::asio::buffer(messageItem.body(), messageItem.bodyLength()), [this, self](boost::system::error_code ec, size_t) {
                if(!ec) {
                    room_.deliver(messageItem);
                    readHeader();
                }
                else {
                    room_.leave(shared_from_this());
                }
            });
        }
        void write() {
            auto self(shared_from_this());
            boost::asio::async_write(socket, boost::asio::buffer(Messages.front().data(), Messages.front().length()), [this, self](boost::system::error_code ec, size_t) {
                if(!ec) {
                    Messages.pop_front();
                    if(!Messages.empty()) {
                        write();
                    }
                }
                else {
                    room_.leave(shared_from_this());
                }
            });
        }
        tcp::socket socket;
        room& room_;
        message messageItem;
        messageQueue Messages;
};
class server {
    public:
        server(boost::asio::io_context& io_context, const tcp::endpoint& endpoint) : acceptor(io_context, endpoint) {
            do_accept();
        }
    private:
        void do_accept() {
            acceptor.async_accept([this](boost::system::error_code ec, tcp::socket socket) {
                if(!ec) {
                    make_shared<session>(move(socket), room_)->start();
                }
                do_accept();
            });
        }
        tcp::acceptor acceptor;
        room room_;
};
int main(int argc, char* argv[]) {
    try {
        if(argc < 2) {
            cerr << "Usage: server <port> [<port> ...]\n";
            return 1;
        }
        boost::asio::io_context io_context;
        list<server> servers;
        for(int i = 1; i < argc; ++i) {
            tcp::endpoint endpoint(tcp::v4(), atoi(argv[i]));
            servers.emplace_back(io_context, endpoint);
        }
        io_context.run();
    }
    catch (exception& e) {
        cerr << "Exception: " << e.what() << "\n";
    }
    return 0;
}

نرم افزار دوم و سمت کاربر : (client.cpp)


#include <iostream>
#include <thread>
#include <cstdlib>
#include <deque>
#include <boost/asio.hpp>
#include "message.hpp"
using boost::asio::ip::tcp;
using namespace std;
typedef deque<message> messageQueue;
class client {
    public:
        client(boost::asio::io_context& context, const tcp::resolver::results_type& endpoints) : context(context), socket(context) {
            connect(endpoints);
        }
        void write(const message& messageItem) {
            boost::asio::post(context, [this, messageItem]() {
                bool write_in_progress = !writeMessage.empty();
                writeMessage.push_back(messageItem);
                if(!write_in_progress) {
                    write();
                }
            });
        }
        void close() {
            boost::asio::post(context, [this]() { socket.close(); });
        }
    private:
        void connect(const tcp::resolver::results_type& endpoints) {
            boost::asio::async_connect(socket, endpoints, [this](boost::system::error_code ec, tcp::endpoint) {
                if(!ec) {
                    readHeader();
                }
            });
        }
        void readHeader() {
            boost::asio::async_read(socket, boost::asio::buffer(readMessage.data(), message::headerLength), [this](boost::system::error_code ec, size_t) {
                if(!ec && readMessage.decodeHeader()) {
                    readBody();
                }
                else {
                    socket.close();
                }
            });
        }
        void readBody() {
            boost::asio::async_read(socket, boost::asio::buffer(readMessage.body(), readMessage.bodyLength()), [this](boost::system::error_code ec, size_t) {
                if(!ec) {
                    cout.write(readMessage.body(), readMessage.bodyLength());
                    cout << "\n";
                    readHeader();
                }
                else {
                    socket.close();
                }
            });
        }
        void write() {
            boost::asio::async_write(socket, boost::asio::buffer(writeMessage.front().data(), writeMessage.front().length()), [this](boost::system::error_code ec, size_t) {
                if(!ec) {
                    writeMessage.pop_front();
                    if(!writeMessage.empty()) {
                        write();
                    }
                }
                else {
                    socket.close();
                }
            });
        }
        boost::asio::io_context& context;
        tcp::socket socket;
        message readMessage;
        messageQueue writeMessage;
};
int main(int argc, char* argv[]) {
    try {
        if(argc != 3) {
            cerr << "Usage: client <host> <port>\n";
            return 1;
        }
        boost::asio::io_context context;
        tcp::resolver resolver(context);
        auto endpoints = resolver.resolve(argv[1], argv[2]);
        client c(context, endpoints);
        thread t([&context](){ context.run(); });
        char line[message::maxBodyLength + 1];
        while(cin.getline(line, message::maxBodyLength + 1)) {
            message messageItem;
            messageItem.bodyLength(strlen(line));
            memcpy(messageItem.body(), line, messageItem.bodyLength());
            messageItem.encodeHeader();
            c.write(messageItem);
        }
        c.close();
        t.join();
    }
    catch (exception& e) {
        cerr << "Exception: " << e.what() << "\n";
    }
    return 0;
}

 

 این پروژه آزمایشی بصورت رایگان و اوپن سورس در اینترنت بخصوص اینجا وجود دارد و می توانید آنرا مستقیما بصورت کامل دانلود کنید. 

 

با تشکر,

Max Base / مکس بیس

قاسم رمضانی منش

زبان نشانه‌گذاری Markdown یا به اختصار MD همانند زبان HTML برای طراحی قالب متن استفاده می‌شود. به گمانم که قبلاً نمونه‌های زیادی را تحت عنوان README.md شنیده‌باشید. مانند نمونهٔ زیر :

image.png.035a420560452d8448cf712c59a62203.png

استفاده از این قالب بسیار ساده‌ است، حال بخشی از نحوهٔ نگارش فایل‌های MD را می‌گوییم. 

برای نوشتن سر‌نویس‌ها (Header) :

Header1
=======

# Header1
## Header2
### Header3

برای "خط‌جدید" (NewLine) از دو کاراکتر Space در انتهای هر پاراگراف استفاده می‌کنیم و یا از دو/n (توسط کلید ReturenKey) استفاده می‌کنیم

first line.

second line.

ویژگی‌های متن :

_italic_ or *italic*
__bold__ or **bold**
`monospace` 

نوشن یک بلاک کد :

```<Language-Name>
   // And Write Code Here
```

```c
int main (int argc, char **argv, char **envp){
    /* some code here */
    return EXIT_SUCCESS;
}
```

or indent your code by four space:
    int main (int argc, char **argv, char **envp){
        /* some code here */
        return EXIT_SUCCESS;
    }

انواع لیست‌ها :

Bullet List:
* One Option
* Another Option


Numbered List:
1. Free Software
2. GNU/Linux

Todo List:
- [x] it's done.
- [ ] working on it.

نوشتن پیوند‌ها :

[GNU/Linux](www.kernel.org)
![Image](https://en.wikipedia.org/wiki/File:Qt_logo_2016.svg)

استفاده از حالت "نقل‌قول" :

> for using blockquoting in md files.

استفاده از جداول :

Prorgamming Language | It's God ?
---------------------|------------
C | Very GOD
C++ | It's God
Python | :(

 

خروجی تمام نمونه‌های بالا :

image.thumb.png.0bd89def81dbc0d6c735e8214a95e70a.png

استفاده از قالب‌های گفته شده، بستگی به پیاده‌سازی موتور رندر ویرایشگری دارد که شما از آن استفاده می‌کنید، چرا که ممکن است تمام قابلیت‌ها را پیاده‌سازی نکرده‌باشد.

موفق‌وپیروز باشید. ?

قاسم رمضانی منش

Image result for runtime error fun pic

بارها بوده که برنامه‌‌ای را نوشته‌ایم، روند کامپایل و اجرا به خوبی و خوشی انجام می‌شود. امّا در مرحلهٔ اجرای برنامه، خروجی‌های نامناسبی پدیدار می‌شود. که متأسفانه چیزی نیستند که ما می‌خواهیم. خب برای حل این مشکل دو راه پیش‌رو می‌باشد :

  • بازبینی کد و انجام تست برای یافتن محل مشکل.
  • استفاده از ابزارهای خطایابی (Debugging).

بازبینی کد و انجام تست برای یافتن محل مشکل

برای این‌کار فریورک‌ها و کتابخانه‌های زیادی موجود می‌باشد، مثلاً کتابخانهٔ تست‌نویسی (Test Case) به اسم Catch2. کار با این کتاخانه بسیار آسان است. برای مثال تابع زیر را در نظر داشته‌باشید :

unsigned int Factorial( unsigned int number ) {
    return number <= 1 ? number : Factorial(number-1)*number;
}

ما می‌خواهیم بدانیم که آیا این تابع خروجی مناسب را دارد یا خیر، درصورتی‌که از catch2 استفاده می‌کنید، کافیست که طبق راهنمای README.md هدرفایل catch.cpp را دریافت و به برنامهٔ خود اضافه کنید :

#define CATCH_CONFIG_MAIN  // This tells Catch to provide a main() - only do this in one cpp file
#include "catch.hpp"

unsigned int Factorial( unsigned int number ) {
    return number <= 1 ? number : Factorial(number-1)*number;
}

TEST_CASE( "Factorials are computed", "[factorial]" ) {
    REQUIRE( Factorial(1) == 1 );
    REQUIRE( Factorial(2) == 2 );
    REQUIRE( Factorial(3) == 6 );
    REQUIRE( Factorial(10) == 3628800 );
}

و بهصورت نمونهٔ کد بالا از catch2 استفاده می‌کنیم. طبق اسناد با تعریف ماکروی CATCH_CONFIG_MAIN ما یک تابع main توسط خود catch2 تعریف می‌کنیم. و کافیه که فقط این سورس را کامپایل و اجرا کنیم :

$> g++ -o catch2Test main.cpp
$> 
$> ./catch2Test
===============================================================================
All tests passed (4 assertions in 1 test case)

و خب مسلماً درصورتی‌که خطایی باشد در این آزمایشات معلوم می‌گردد،‌ فریمورک‌های دیگری مانند Google Test نیز موجود می‌باشد.

 

استفاده از ابزارهای خطایابی (Debugging)

در این روش شما برنامهٔ خود را تحت برنامهٔ دیگری اجرا و اقدام به خطایابی می‌کنید، یکی از برنامه‌های خطایابی معروف و آزاد، برنامهٔ GNU Debugger می‌باشد. از این برنامه برای خطایابی در زبان‌های  :

  • Ada
  • Assembly
  • C
  • ++C
  • D
  • Fortran
  • Go
  • Objective-C
  • OpenCL
  • Modula-2
  • Pascal
  • Rust

استفاده می‌شود.

برای نصب می‌توانید از مدیربستهٔ سیستم‌عامل خود استفاده کنید :

$> apt install gdb gdb-doc

و یا اینکه از این پیوند برنامه را دریافت و اقدام به کامپایل/نصب آن کنید.

نکته : دقت کنید که همیشه نباید حتماً خطایی در برنامه باشد تا اقدام به خطایابی کنیم، گاهی هم نیاز است که روند کار برنامه را به این روش با استفاده از ابزارهای مشابه پی‌گیری کنیم. (In fact: reverse engineering)

حال اقدام به Debug کردن این برنامهٔ کوتاه می‌نماییم :

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>

static int data = 0x02A;

int
main (void){
    int stack = 0x2FF;
    pid_t pid;

    switch (pid = fork()){
        case -1:
            perror("pid = fork()");
            exit(EXIT_FAILURE);
        case 0:
            data  |= 0x02B;
            stack &= data;
            break;
    }

    fprintf(stdout, "[%s]\v [PID=%ld] [data=%d] [stack=%d].\n",
            (pid == 0) ? "child" : "parent", (long) getpid(),
            data, stack);
    exit(EXIT_SUCCESS);
}

قبل‌از اینکه برنامه‌ای را برای دیباگ‌ کردن داخل GDB استفاده کنیم،‌ نیاز است که برای کمک به این روند اطلاعاتی مانند مکان Source Code برنامه را به فایل باینری خود اضافه کنیم. برای اینکار کافیست که از فلگ -g یا -ggdb یا -g3 استفاده کنیم. این پیوند را برای اطلاعات بیشتر مطالعه کنید.

به این‌صورت برنامه را کامپایل و برای دیباگ کردن آماده می‌کنیم :

$> gcc -o output -ggdb main.c

توجه کنید که سورس برنامه را از مکانش تغییر ندهید. حال برنامهٔ gdb را با نوشتن اسم gdb در shell فراخوانی می‌کنیم :

$> gdb
GNU gdb (Debian 8.2.1-2) 8.2.1
Copyright (C) 2018 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
    <http://www.gnu.org/software/gdb/documentation/>.

For help, type "help".
Type "apropos word" to search for commands related to "word".
(gdb)

برای اینکه فایل باینری را باز کنیم باید از دستور file استفاده کنیم. همچنین می‌توانیم به این‌صورت نیز برنامهٔ gdb را به همراه فایل باینری خود اجرا نماییم :

$> gdb output

خب،‌ در این قسمت می‌توانیم با استفاده از دستور run برنامهٔ خودمان را اجرا کنیم و درصورتی‌که نیاز باشد آرگومان‌هایی را نیز ارسال کنیم. وقتی دستور run‌ را وارد می‌کنیم برنامهٔ ما اجرا می‌شود و خاتمه میابد. زمانی‌که نیاز داریم روند اجرای برنامه در نقطه‌های مشخصی متوقف شود باید از ‌Break Point استفاده کنیم. برای قرار دادن Break Point در خط‌های مختلف برنامه از دستور break به اضافهٔ شمارهٔ خط سورس برنامه استفاده می‌کنیم. 

برای اینکار بد نیست که اطلاعاتی نیز درمورد سورس‌کد خود داشته‌باشیم مثلاً همزمان بتوانیم سورس را نیز مشاهده کنیم، درصورتی‌که از ادیتور Emacs استفاده می‌کنید می‌توانید M+x را زده و gdb را اجرا کنید. که یک بافر در سمت چپ برای شما باز می‌کند (درصورتی‌که دکمه‌های Ctrl + X و Ctrl + 3 را زده باشید). در غیر این‌صورت به دو روش دیگر می‌توانید سورس خود را هنگام دیباگ مشاهده کنید، در روش اوّل استفاده از دستور list به اضافهٔ دو آرگومان می‌باشد :

  • آرگومان اوّل مشخص کنندهٔ خط شروع است، و آرگومان دوّم مشخص کننده خط پایان هست. مثلاً با فراخوانی list 2, 10 از خط دو تا ده را نمایش می‌دهد :
(gdb) list 2, 10
2	#include <stdlib.h>
3	#include <sys/types.h>
4	#include <unistd.h>
5	
6	static int data = 0x02A;
7	
8	int
9	main (void){
10	    int stack = 0x2FF;
  • می‌توانید فقط یک آرگومان ارسال کنید، که به اندازه ی مقدار متغیر listsize که پیشفرض ده می‌باشد (می‌توانید با استفاده از دستور set مقدار را تغییر دهید) را نمایش می‌دهد :
(gdb) list 2
1	#include <stdio.h>
2	#include <stdlib.h>
3	#include <sys/types.h>
4	#include <unistd.h>
5	
6	static int data = 0x02A;
7	
8	int
9	main (void){
10	    int stack = 0x2FF;

حالات دیگه‌ای هم دارد که می‌توانید با وارد کردن help list  متوجه شوید.

امّا روش بهتری (روش دوّم) نیز برای دیدن سورس برنامه به همراه دیباگ کردن وجود دارد،‌ می‌توانید از رابط TUI استفاده کنید. برای استفاده از این رابط دکمه‌های Ctrl + X و Ctrl + A یا < وارد کرده و ReturnKey (یا همان Enter) را بزنید. در این قسمت شما به راحتی می‌توانید هم سورس برنامهٔ خودتان را ببینید و هم برنامه را دیباگ کنید. با یک‌بار اجرا کردن برنامه توسط دستور run سورس برنامه‌ٔ شما بارگذاری می‌شود.

2019-05-21_22-13.thumb.png.50f113d47c64e7b3fed972e680c7776e.png

برویم به سراغ Break Point گذاشتن، برای مثال ما می‌خواهیم یک Break Point بر سر خط ۱۰ و ۱۳ بگذاریم :

(gdb) break 11
Breakpoint 1 at 0x555555555185: file main.c, line 13.
(gdb) break 10
Breakpoint 2 at 0x55555555517e: file main.c, line 10.
(gdb) 

دوباره برنامه را با وارد کردن دستور run اجرا می‌کنیم، تا اجرای برنامه این‌بار در برخورد با اوّلین Break Point متوقف شود :

2019-05-21_22-26.thumb.png.828b6f29565d27aa27b19c77181dd0a3.png

در این توقف، ما می‌توانیم با استفاده از دستور print محتویات متغیرهارا مشاهده کنیم :

(gdb) print data
$1 = 42
(gdb) print stack
$2 = 21845

دستور print قابلیت‌های جالبی دارد :

(gdb) print 12 / 2
$1 = 6
(gdb) print sizeof(int)
$2 = 4
(gdb) print &data
$3 = (int *) 0x4a60f0 <data>
(gdb) 

مقادیری که به ترتیب در سمت چپ شماره‌گذاری شده‌اند درواقع اسم متغیرهایی هستنند که خروجی در آن قرار گرفته است :

(gdb) print $1
$4 = 42
(gdb) print $4
$5 = 42
(gdb) 

همچنین با استفاده از دستور delete می‌توانیم یک Break Point را حذف کنیم. برای ادامه دادن به روند اجرای برنامه تا Break Point  بعدی از دستور continue و برای رفتن به خط بعدی از دستور next استفاده می‌کنیم.بعد از اجرای دستور next دقت کنید سریعاً به خط 23 رفته و فراخوانی تابع سیستمی fork() را رها می‌کند. به خاطر اینکه دستور next کاری به توابعی که فراخوانی کرده‌اید، در اینجا فراخوان سیستمی fork() ، ندارد و دستورات سورس شما را ادامه می‌دهد؛ امّا درصورتی‌که از step یا stepi استفاده بکنید وارد دستورات شده و دستورات توابع شما را پیمایش می‌کند:

(gdb) next
[child]
        [PID=563] [data=43] [stack=43].
[Detaching after fork from child process 563]
(gdb) 

نکته : رابط TUI زیاد قوی نمی‌باشد،‌ لذا درصورتی‌که خروجی چاپ شود تنظیمات صفحه نمایش را به هم می‌زد می‌توانید با زدن Ctrl + L خروجی‌های اضافه را از بین ببرید.

برای دیباگ کردن یک fork() می‌توانید مقدار follow-fork-mode را ویرایش کنید :

(gdb) set follow-fork-mode child 
(gdb) run
Starting program: /tmp/output

Breakpoint 2, main () at main.c:10
(gdb) next

Breakpoint 1, main () at main.c:13
(gdb) step
[Attaching after process 2387 fork to child process 2403]
[New inferior 2 (process 2403)]
[Detaching after fork from parent process 2387]
[Inferior 1 (process 2387) detached]
[Switching to process 2403]
main () at main.c:13
(gdb) 

همچنین با استفاده از دستور disassemble می‌توانید سورس اسمبلی یکی تابع را مشاهده کنید :

(gdb) disassmble main
Dump of assembler code for function main:
   0x0000000000401b4d <+0>:     push   %rbp
   0x0000000000401b4e <+1>:     mov    %rsp,%rbp
   0x0000000000401b51 <+4>:     push   %rbx
   0x0000000000401b52 <+5>:     sub    $0x18,%rsp
=> 0x0000000000401b56 <+9>:     movl   $0x2ff,-0x14(%rbp)
   0x0000000000401b5d <+16>:    callq  0x43c9b0 <fork>
   0x0000000000401b62 <+21>:    mov    %eax,-0x18(%rbp)
   0x0000000000401b65 <+24>:    cmpl   $0xffffffff,-0x18(%rbp)
   0x0000000000401b69 <+28>:    je     0x401b73 <main+38>
   0x0000000000401b6b <+30>:    cmpl   $0x0,-0x18(%rbp)
   0x0000000000401b6f <+34>:    je     0x401b89 <main+60>
   0x0000000000401b71 <+36>:    jmp    0x401ba2 <main+85>
   0x0000000000401b73 <+38>:    lea    0x7c48e(%rip),%rdi        # 0x47e008
   0x0000000000401b7a <+45>:    callq  0x408bd0 <perror>
   0x0000000000401b7f <+50>:    mov    $0x1,%edi
   0x0000000000401b84 <+55>:    callq  0x408010 <exit>
   0x0000000000401b89 <+60>:    mov    0xa4561(%rip),%eax        # 0x4a60f0 <da
--Type <RET> for more, q to quit, c to continue without paging--

لینک منبع را برای ادامهٔ داستان دنبال کنید :).

موفق و پیروز باشید.?

قاسم رمضانی منش

چندی پیش یکی از دوستان درمورد کتابخانهٔ zlib از من سوأل پرسیده بود که جالب شد برایم تا نگاهی بکنم. zlib یک کتابخانهٔ فشرده‌سازی بر اساس الگوریتم DEFLATE هست که خود این الگوریتم تلفیقی از LZ77 و الگوریتم Huffman هست و عمل فشرده‌سازی‌درحافظه را انجام می‌دهد، اطلاعات بیشتر درمورد اینا خواستید از این‌جا استفاده کنید. این کتابخانه یک قسمت مهم از پلتفرم‌های معروفی همچون GNU/Linux , iOS و.. هست.

تستی که با این کتابخانه انجام دادم واقعاً برایم جالب بود، یک فایل متنی ۶۰۰ مگابایتی را به ۱۲۱ مگابایت رسوند در مدّت زمان خیلی کوتاهی با یک پردازندهٔ Intel(R) Core(TM) i7 CPU M 620. خب بریم یک تست بکنیم. اوّل سورس برنامه را از این قسمت بارگیری کنید :

https://www.zlib.net/

توضیحات مفصل را می‌خواید می‌توانید از این‌قسمت استفاده کنید. امّا من از یک‌مثال استفاده می‌کنم، بعد از اینکه سورس‌کد را دانلود کردید کافیه که از حالت فشرده خارجش کنید و وارد دایرکتوری مربوطه‌اش بشید. برای کامپایل شما نیاز به :

  1. GCC
  2. GNU Make

دارید، اگر نمی‌دانید GNU Make چی هست، می‌توانید در این‌قسمت با GNU Make آشنا بشید. خب اگر ابتدا برنامهٔ make را داخل دایرکتوری فراخوانی کنید پیغام زیر را نمایش میدهد :

Please use ./configure first.  Thank you.

که مؤدبانه خواهش می‌کند اوّل اسکریپت configure را اجرا کنیم، بعد از اجرای این اسکریپت چک‌های لازم انجام می‌شود و بعد می‌توانید make را اجرا کنید تا کتابخانه‌‌های مورد نظر ساخته بشود. بعد اتمام کار، ما فقط نیاز به Shared lib ها و Header File مربوطه داریم. (درصورتی‌که نمی‌دانید Shared Lib چیست، می‌توانید در این‌قسمت با نحوهٔ‌کار/ساخت آن آشنا شوید). پس بهتر است یک دایرکتوری به اسم lib در ساختار دایرکتوری پروژهٔ خودمان درست کنیم و به این‌صورت عمل کنیم :

zlib-1.2.11$> mkdir ../zlibTEST/lib
zlib-1.2.11$> mv libz*so* ../zlibTEST/lib
renamed 'libz.so' -> '../zlibTEST/lib/libz.so'
renamed 'libz.so.1' -> '../zlibTEST/lib/libz.so.1'
renamed 'libz.so.1.2.11' -> '../zlibTEST/lib/libz.so.1.2.11'
zlib-1.2.11$> mv zlib.h ../zlibTEST/header
renamed 'zlib.h' -> '../zlibTEST/header/zlib.h'

در این قسمت، اوّل ما خارج‌از دایرکتوری zlib داخل یک دایرکتوری دیگر که پروژهٔ ما درآن قرار دارد یک دایرکتوری به اسم lib ساختیم که shared lib و header file مربوطه را درآن قرار دهیم. سپس تمام فایل‌هایی که به اسم 'libz*so*' هستند را به آن دایرکتوری انتقال دادیم؛ سه فایل قرار دارد که دو تا از آنها به libz.so.1.2.11 لینک شده‌اند. 

نقل قول

میدانیم که Shared LIbها فقط یک‌بار در سیستم‌عامل قرار می‌گیرند، و تمام برنامه‌ها به آن لینک می‌شوند. خب اگر قرار باشد که هر Shared Lib برنامه‌ای یک اسم در هر آپدیت بگیرد. مثلا :

  • lib.so.1.2.11
  • lib.so.1.2.12 ...

مشکل‌ساز می‌شود، چرا که آن‌برنامه دیگر نمی‌تواند از آخرین نسخهٔ کتابخانه استفاده کند و برای استفاده باید دوباره کامپایل شود، از این رو هر کتابخانه به یک اسم نوشته‌میشود مثلاً libz.so و این فایل به فایل‌های اصلی لینک می‌شود. لکن دیگر شما فقط به libz.so برنامهٔ‌تان را لینک می‌کنید و توسعه‌دهندهٔ کتابخانه در هربار آپدیت فقط آن فایلی را کهlibz.so به آن اشاره می‌کند را تغییر می‌دهد و برنامه‌های دیگر همیشه با آخرین آپدیت همراه هستند.

خب بریم سروقت تست خودمان.

اوّل از همه نیاز به هدرفایل‌های مربوطه داریم :

#include <stdio.h>
#include <string.h>
#include <assert.h>
#include "zlib.h"

کتابخانهٔ zlib از ثابت CHUNK برای مقداردهی Buffer خودش استفاده می‌کنه، و ما نیاز داریم که این ثابت را تعریف کنیم :

#define CHUNK 251904

هرچی مقدار بیشتر باشه سیستم کارآمد تر هست، داخل خود اسناد گفته که بهتره از 256k استفاده کنیم درصورتی‌که مقدار حافظهٔ موردنیاز رو داریم. حالا باید تابع Compressing خودمان را با استفاده از کتابخانهٔ zlib پیاده کنیم. ما اسم این تابع را compressing میزاریم، این تابع دو stream دریافت می‌کند که یکی ورودی و یکی خروجی می‌باشد. یک ورودی دیگر تابع سطح فشرده‌سازی هست که در ادامه بحث می‌کنیم :

int
compressing (FILE *source, FILE *dest, int level);

خروجی‌‍ه تابع می‌تواند این موارد باشد :

- Z_OK            = 0
- Z_STREAM_END    = 1
- Z_NEED_DICT     = 2
- Z_ERRNO         = -1
- Z_STREAM_ERROR  = -2
- Z_DATA_ERROR    = -3
- Z_MEM_ERROR     = -4
- Z_BUF_ERROR     = -5
- Z_VERSION_ERROR = -6

از اسامی تقریباً مشخص هست که چه مفهومی دارند و نیازی به توضیح نیست. حال نیاز هست که یک سری متغیر‌های‌محلی که فقط مورد استفادهٔ خود تابع هست را داخل تابع تعریف کنیم :

    int return_;
    int flush;
    int have;
    z_stream stream;
    unsigned char input[CHUNK];
    unsigned char output[CHUNK];

متغیر اوّل که از اسم‌‍ش مشخص هست، برای مشخص کردن مقداربازگشتی از تابع هست، و متغیر دوّم برای مشخص کردن وضعیّت flushing برای یکی از توابع zlib هست.

نقل قول

زمانی که شما مقداری را مثلاً می‌خواهید داخل یک stream بریزید، بلافاصله انجام نمی‌شود. این مقدار داخل Buffer نگه‌داری می‌شود تا زمانی که تابع fflush() صدا زده شود.

متغیر سوّم مقدار اطلاعاتی هست که از یکی از توابع zlib به اسم deflate() بر می‌گردد. متغیر چهارم هم از نوع یک ساختار داخلیه zlib می‌باشد :

typedef struct z_stream_s {
    z_const Bytef *next_in;     /* next input byte */
    uInt     avail_in;  /* number of bytes available at next_in */
    uLong    total_in;  /* total number of input bytes read so far */

    Bytef    *next_out; /* next output byte will go here */
    uInt     avail_out; /* remaining free space at next_out */
    uLong    total_out; /* total number of bytes output so far */

    z_const char *msg;  /* last error message, NULL if no error */
    struct internal_state FAR *state; /* not visible by applications */

    alloc_func zalloc;  /* used to allocate the internal state */
    free_func  zfree;   /* used to free the internal state */
    voidpf     opaque;  /* private data object passed to zalloc and zfree */

    int     data_type;  /* best guess about the data type: binary or text
                           for deflate, or the decoding state for inflate */
    uLong   adler;      /* Adler-32 or CRC-32 value of the uncompressed data */
    uLong   reserved;   /* reserved for future use */
} z_stream;

توضیحات‌‍َش داده شده داخل خودzlib.h که این ساختار به چه شکلی هست و هر مقدار برای چه کاری هست. و دو متغیر بعدی بافرهای ورودی و خروجی‌ما می‌باشد.

کتابخانهٔ zlib از روش تخصیص‌حافظهٔ به خصوص خود استفاده می‌کند، از این رو باید ساختاری که ساخته‌ایم را با استفاده از تابع deflateInit() مقداردهی کنیم، قبل از مقداردهی باید یک‌سری مقادیر را طبق گفتهٔ مستندات برابر Z_NULL قرار بدهیم :

    stream.zalloc = Z_NULL;
    stream.zfree  = Z_NULL;
    stream.opaque = Z_NULL;
    return_ = deflateInit(&stream, *level);
    if (return_ != Z_OK)
        return return_;

در اینجا مقدار level می‌تواند چیزی بین -1 تا 9 باشد، هرچه مقدار کم‌تر باشد سرعت فشرده‌سازی بالاتر است و مقدارفشرده‌سازی کم‌تر. مقدار صفر هیچ فشرده‌سازی‌ای انجام نمی‌شود و صرفاً یک فایل با فرمت zlib درست می‌شود. ماکروی Z_DEFAULT_COMPRESSION برابر با -1 هست که سرعت و فشره‌سازی خوبی را فراهم کند.  در تست قبلی خودم مقدار را برابر Z_DEFAULT_COMPRESSION گذاشتم و این‌بار می‌خواهم برابر ۹ بگذارم.

خب حالا باید بریم سراغ فشرده‌سازی، در این قسمت ما یکdo-while می‌نویسیم که جریان‌ورودی را تا EOF (انتهای فایل) بخواند :

    do{
        stream.avail_in = fread(input, 1, CHUNK, source);
        if (ferror(source)){
            deflateEnd(&stream);
            return Z_ERRNO;
        }
        flush = feof(source) ? Z_FINISH : Z_NO_FLUSH;
        stream.next_in = input;

* خب برای دوستانی که با توابع کار با Streamها در سی آشنا هستند، پیشنهاد می‌کنم که این قسمت رو یه‌کمی ازش گذر کنند.

اوّل ما از تابع fread استفاده کردیم، این تابع به این‌صورت در فایل stdio.h تعریف شده است :

size_t
fread( void *restrict buffer, size_t size, size_t count, 
       FILE *restrict stream );

و کاری که می‌کند این است که اوّل یک اشاره‌گر به جایی که باید داده‌‌های خوانده شده ذخیره بشوند می‌گیرید که اینجا ما آرایهٔ input را می‌دهیم، سپس اندازهٔ هر داده‌ای که قرار است خوانده بشود را دریافت می‌کند که یک بایت است هر کاراکتر، آرگومان بعدی مقداری است که باید از Stream خوانده شود که ما به اندازهٔ CHUNKتا می‌خواهیم :). آرگومان آخری نیز که مشخص‌هست. جریانی است که باید داده‌ها خوانده شود.

این تابع مقدار داده‌هایی را که با موفقیت خوانده‌است را برمی‌گرداند. که ما آن را در stream.avail_in نگه‌داری می‌کنیم. سپس باید Stream را چک کنیم که خطایی رخ نداده باشد. درصورتی‌که این تابع مقداری غیراز صفر برگرداند مشخص است که خطایی رخ نداده. و درصورتی‌که خطایی رخ داده‌باشد با استفاده از delfateEnd() جریان را پایان می‌دهیم. 

و در انتها باید بررسی کنیم که آیا جریان‌ما به EOF (پایان فایل) رسیده‌است یا خیر. که اینکار با استفاده از تابع feof() در هدرفایل stdio.h صورت می‌گیرد. درصورتی‌که پایان‌فایل رسیده باشد مقداری غیر از صفر این تابع بر می‌گرداند.

در انتها طبق گفتهٔ مستندات باید اشاره‌گری به داده‌های خوانده شده در next_in قرار بگیرد. که ما اینکار را در خط آخر انجام داده‌ایم.

خب در این‌قسمت که ما داده‌ها را از جریان ورودی خواندیم نیاز هست که با استفاده از تابع deflate() عمل فشرده‌سازی را انجام دهیم. این تابع داخل یک حلقهٔ do-while دیگر فراخوانی می‌شود. و تا انتهای داده‌های خوانده شده ادامه می‌دهیم :

    do{
        stream.avail_out = CHUNK;
        stream.next_out  = output;

مقدار فضای output‌ما که برای deflate() تهیه شده است توسط avail_out به بایت مشخص می‌‌شود و next_out اشاره‌گری به آن جریان خروجی می‌باشد که در اینجا آرایهٔ output می‌باشد. 

خب حالا ما باید تابع فشرده‌سازی deflate() را فراخوانی کنیم. این تابع به اندازهٔ avail_in بایت از next_in پردازش می‌کند و به اندازهٔ avail_out بایت در next_out می‌نویسد. که اینجا مقادیر avail_out/in ما برابر با CHUNK می‌باشد و next_out/in ما به  آرایه‌های input و output اشاره‌ می‌کند. این حلقهٔ داخلی‌که درست کردیم تضمین می‌کند که تمام داده‌های خوانده‌شده پردازش و نوشته می‌شوند.

ورودی‌های تابع deflate() یک اشاره‌گر به ساختار z_stream می‌باشد (همان متغیر stream خودمان) و یک ورودی دیگر که مشخص می‌کند وضعیت و چگونگی flush کردن داده‌ها در output. تابع deflate() تا زمانی‌که مقدار ورودی flush state برابر Z_NO_FLUSH باشد ادامه می‌دهد و وقتی‌که مقدار flush state برابر Z_FINISH تابع deflate() کار را تمام می‌‌کند. این قسمت برای افرادی هست که می‌خواهند کارهای خاصی با این تابع انجام دهند که بدین منظور بهتر است مستندات فنی کتابخانه را مطالعه کنند.

        return_ = deflate(&stream, flush);
        assert(return_ != Z_STREAM_ERROR);

در اینجا ما با استفاده از ماکروی assert که در هدرفایل assert.h تعریف شده است یک شرط می‌گذاریم که درصورتی‌که آن شرط حاصل‌‍ش برابر صفر باشد مقادیری را در stderr چاپ و با استفاده از abort() برنامه را خاتمه می‌دهد.

خب حالا باید مشخص کنیم که تابع ‌deflate() در آخرین فراخوانی چه مقدار خروجی تولید کرده‌است و چه مقدار باقی‌مانده است. و مقادیر تولید شده را داخل جریان خروجی می‌نویسیم :

        have = CHUNK - stream.avail_out;
        if (fwrite(output, 1, have, dest) != have || ferror(dest)) {
            deflateEnd (&stream);
            return Z_ERRNO;
        }

در اینجا ما با استفاده از تابع fwrite (که ورودی‌های آن مشابه fread می‌باشند) مقدار تولید شده را داخل جریان خروجی می‌نویسیم. این تابع باید تعداد مقادیری که با موفقیت نوشته شده‌اند را به بایت بر گرداند. پس بررسی می‌کنیم که اگر برابر با have نبود یا اینکه برای جریان dest خطایی رخ داده است. برنامه را خاتمه دهد. تابع deflate() تا جایی که بتواند به کارخود ادامه می‌دهد و زمانی که دیگر داده‌ای برای پردازش نداشته‌باشد مقدار avail_out برابر صفر قرار می‌گیرد و مقدار Z_BUF_ERROR را بر می‌گرداند. و ما می‌توانیم از حلقهٔ داخلی خارج شویم :

     } while (stream.avail_out == 0);
     assert(stream.avail_in == 0);

خب ما با بررسی متغیر flsuh می‌توانیم وضعیت پایان فایل را متوجه بشویم، درصورتی‌که مقدار این متغیر برابر Z_FINISH باشد کار ما تمام شده‌است و می‌توانیم از حلقه خارج شویم :

   } while (flush != Z_FINISH);
   assert(return_ == Z_STREAM_END);

و در انتها کافی است که حافظه‌ای که دریافت شده آزاد شود، و مقدار Z_OK از تابع برگرداننده شود :

    deflateEnd(&stream);
    return Z_OK;
}

 

خب تابع compress ما به اتمام رسید، حال باید بریم سروقت تابع decompress،‌ این تابع شباهت بسیار زیادی به تابع قبلی دارد :

int
decompress (FILE *source, FILE *dest);

و حالا متغیر‌های‌محلی را دوباره تعریف می‌کنم، اینجا دیگر نیازی به متغیر flush نیست چرا که خود توابع zlib پایان کار را مشخص می‌کنند :

{
    int return_;
    unsigned have;
    z_stream stream;
    unsigned char input[CHUNK];
    unsigned char output[CHUNK];

و حال نیاز هست که زمینهٔ تخصیص حافظه را فراهم کنیم :

    stream.zalloc = Z_NULL;
    stream.zfree  = Z_NULL;
    stream.opaque = Z_NULL;
    stream.avail_in = 0;
    stream.next_in  = Z_NULL;
    return_ = inflateInit(&stream);
    if (return_ != Z_OK)
        return return_;

اینجا مقدار avail_in برابر صفر و مقدار next_in برابر Z_NULL قرار می‌گیرد تا مشخص شود که هیچ ورودی فراهم نشده است.

حالا باید حلقهٔ معروف خودمان را درست کنیم و با استفاده از تابع inflate() اقدام به Decompressing کنیم :

    do {
        stream.avail_in = fread(input, 1, CHUNK, source);
        if (ferror(source)){
            inflateEnd(&stream);
            return Z_ERRNO;
        }
        if (stream.avail_in == 0)
            break;
        stream.next_in = input;

خب با توجه به توضیحات تابع قبلی این دستورات نیز عملکردشان مشخص است. حال باید حلقهٔ‌داخلی را بنویسیم :

        do {
            stream.avail_out = CHUNK;
            stream.next_out  = output;

حال باید تابع inflate() را برای عمل Decompressing فراخوانی کنیم، دیگر اینجا نیازی  به مشخص کردن flush state نداریم چرا که خود zlib به طور خودکار مدیریت می‌کند. تنها چیزی که مهم است، خروجی تابع inflate() می‌باشد که درصورتی‌که برابر Z_DATA_ERROR باشد به معنی این‌است که در داده‌های فشرده‌شده مشکلی وجود دارد. و خروجی دیگر Z_MEM_ERROR می‌باشد که مشخص‌کنندهٔ مشکلی در زمان حافظه‌گیری برای inflate() می‌باشد :

        return_ = inflate(&stream, Z_NO_FLUSH);
        assert(return_ != Z_STREAM_ERROR);
        switch (return_){
             case Z_NEED_DICT:
                 return_ = Z_DATA_ERROR;
             case Z_DATA_ERROR:
             case Z_MEM_ERROR:
                 inflateEnd(&stream);
                 return return_;
        }

در اینجا خروجی تابع را بررسی کرده و درصورتی‌که خطایی باشد جریان برنامه را خاتمه می‌دهیم.

 

و انتهای حلقه :

        have = CHUNK - stream.avail_out;
        if (fwrite(output, 1, have, dest) != have || ferror(dest)) {
            inflateEnd(&stream);
            return Z_ERRNO;
        }
    } while (stream.avail_out == 0);

و زمانی‌که خروجی‌‍ه inflate() برابر Z_STREAM_END باشد، یعنی اینکه دیگر کار تمام شده و داده‌ای برای پردازش نمی‌باشد :

    } while (return_ != Z_STREAM_END);

تا این قسمت دیگر کار استخراج به پایان رسیده‌ است . و کار تابع decompress را تمام می‌کنیم :

    inflateEnd(&stream);
    return (return_ == Z_STREAM_END) ? Z_OK : Z_DATA_ERROR;
}

 

خب تمام شد !. ما دوتابع compress و decompress که مستقیم از zlib استفاده می‌کنند را به پایان رساندیم. حال بیاید از آنها استفاده کنیم.

در وهلهٔ اوّل نیاز است که تابعی داشته‌باشیم تا خروجی این توابع را برای ما مدیریت کنند :

void
zlibError(int return_) {
  fprintf(stderr, "ZLIB ERROR: ");
  switch (return_) {
  case Z_ERRNO:
    if (ferror(stdin))
      fprintf(stderr, "ERROR READING stdin.\n");
    if (ferror(stdout))
      fprintf(stderr, "ERROR WRITING stdout.\n");
    break;
  case Z_STREAM_ERROR:
    fprintf(stderr, "INVALID COMPRESSION LEVEL.\n");
    break;
  case Z_DATA_ERROR:
    fprintf(stderr, "INVALID OR INCOMPLETE deflate() DATA.\n");
    break;
  case Z_MEM_ERROR:
    fprintf(stderr, "OUT OF MEMORY.\n");
    break;
  case Z_VERSION_ERROR:
    fprintf(stderr, "zlib VERSION MISMATCH.\n");
  }
}

و حال تابع main برنامهٔ ما :

int
main(int argc, char **argv) {
  int return_;

  if (argc == 1) {
    return_ = compress(stdin, stdout, 9);
    if (return_ != Z_OK)
      zlibError(return_);
    return return_;
  } else if (argc == 2 && strcmp(argv[1], "-d") == 0) {
    return_ = decompress(stdin, stdout);
    if (return_ != Z_OK)
      zlibError(return_);
    return return_;
  } else {
    fprintf(stderr, "zlib Usage: PROGRAMM [-d] < SOURCE > DEST\n");
    return EXIT_FAILURE;
  }

  return EXIT_FAILURE;
}

و برای کامپایل باید موقعیت کتابخانهٔ zlib را مشخص کنیم :

$> gcc main.c -L. -lz -O3 -o zlib

خب حالا بیاید با هم این برنامه را اجرا کنیم :). قبل از اجرا نیاز است که ما یک فایل حجیم داشته‌باشیم، برای اینکار کافیه که به این‌صورت یکی درست کنیم :

$> yes "iostram.ir" > huge.file

بهتر از بعد از چند ثانیه با استفاده از Ctrl + C برنامه را خاتمه دهید، برای من بعد از ۱۱ ثانیه برنامهٔ yes فایلی به اندازهٔ ۶۲۹ مگابایت، محتوی iostream.ir درست کرد. حالا بریم برای فشرده‌سازی  :

$> time ./zlib < huge.file > huge.file.comp

real	0m13.560s
user	0m5.785s
sys	0m0.375s

من این برنامه با استفاده از برنامهٔ time اجرا کردم تا زمان مصرفی را مشاهده کنم، که بعد از ۱۳ ثانیه به اتمام رسید. حال بیاید بیبنیم حجم خروجی چقدر است !

$> ls -ltrh
total 631M
-rw-r--r-- 1 ghasem ghasem  94K Jan 15  2017 zlib.h
-rwxr-xr-x 1 ghasem ghasem 119K May 10 10:59 libz.so.1.2.11
lrwxrwxrwx 1 ghasem ghasem   14 May 10 10:59 libz.so.1 -> libz.so.1.2.11
lrwxrwxrwx 1 ghasem ghasem   14 May 10 10:59 libz.so -> libz.so.1.2.11
-rw-r--r-- 1 ghasem ghasem 3.5K May 10 14:39 main.c
-rwxr-xr-x 1 ghasem ghasem  18K May 10 14:40 output
-rwxr-xr-x 1 ghasem ghasem  18K May 10 14:46 zlib
-rw-r--r-- 1 ghasem ghasem 629M May 10 14:46 huge.file
-rw-r--r-- 1 ghasem ghasem 1.3M May 10 14:47 huge.file.comp

واقعاً عالی بود. حجم فایل خروجی برابر با 1.3 مگابایت است. یعنی یک مگابایت و ۳۰۰ کیوبایت. حال بیاید از حالت فشرده خارج کنیم فایل را :

$> time ./zlib -d < huge.file.comp > huge.file.dcomp

real	0m12.556s
user	0m0.818s
sys	0m0.472s

بعد از تنها ۱۳ ثانیه یک فایل ۶۲۹ مگابایتی برایمان درست کرد. که عیناً برابر فایل اوّلی می‌باشد. باور نمی‌کنید ؟ خب بیاید sha1sum آنها برررسی کنیم :

$> sha1sum huge.file
3c02d5bd13b91f0e663d63d11ee33a2e71126615  huge.file
$> sha1sum huge.file > huge.file.sha1
$> sha1sum huge.file.dcomp > huge.file.dcomp.sha1
$> cat huge*.sha1
3c02d5bd13b91f0e663d63d11ee33a2e71126615  huge.file.dcomp
3c02d5bd13b91f0e663d63d11ee33a2e71126615  huge.file

سورس کامل برنامه را از این‌قسمت می‌توانید بارگیری کنید.

 

- موفق‌وپیروز باشید ?

قاسم رمضانی منش

فایل‌ها/تغییرات پروژه را چطوری کنترل کنیم ؟

در وهلهٔ اوّل شاید بگید چه نیازیه ؟ خب برنامه رو می‌نویسیم و میریم دیگه !. درسته برنامه‌اتون را می‌نویسید و می‌روید؛ امّا به کجا چنین شتابان ؟ آیا همیشه برنامهٔ شما کوچک‌خواهد بود ؟ آیا قراره برنامهٔ شما در صد خط تمام بشه ؟ یا اینکه کلاً قصد توسعه‌اش رو دیگه ندارید ؟ خب شاید یکی دیگه داشت :).

فرض کنید برنامهٔ‌تان را نوشتید :

void parsing(int argc, char **argv){
    top = 0;
    for (unsigned i=1; i < (unsigned)argc; i+=2){
        listArgs[top].name    = argv[i];
        listArgs[top].value   = argv[i+1];
        top++;
    }
}

خب، برنامه‌کار می‌کنه و میرید و یک هفته‌ٔ دیگه میاید و مثلاً خط : top = 0; را حذف می‌کنید. و برنامه در اجرای اوّل درست کار می‌کنه؛ پیش‌خودتون می‌گید خب چه نیازی بود، الکی ماهم کد نوشتیم :). امّا بعداً در اجراهای متوالی برنامه شروع می‌کنه به دادن خروجی‌های نامتعارف. اینجاس که باید ساعت‌ها وقت بزارید و بگردید ببینید آخرین‌بار چه چیزی رو تغییر دادید و کدوم فایل را ویرایش کردید.

کار مسخره‌ و اعصاب‌خورد کنی میشه، درسته ؟. امّا برای مدیریت این دَنگٌ‌وفَنگ‌ها می‌تونید از سیستم‌های‌مدیریتپروژه‌ استفاده بکنید. مثل Git حالا اینکه چرا گیت ؟ به خاطر اینکه راحت‌ترینه و بهترینه. چرا ؟ چون امتحانش را پس داده، پروژهٔ بزرگ "کرنل‌لینوکس" را داره مدیریت می‌کنه.

حالا بیاید ببینیم اگه ما ازت گیت (git) استفاده می‌کردیم، چطوری می‌توانستیم بفهمیم که چه بلایی سر کد آمده :

۱- اوّل گزارشات را چک می‌کنم، تا ببینم آخرین گزارشی که از تغییرات ذخیره کردم چه بوده ؟:

$> git log
commit bb513a5f9ec429222de03afa690e7fa5d2fbdf6e (HEAD -> master)
Author: Ghasem Ramezani <g1999ramezani@gmail.com>
Date:   Sun May 5 00:05:22 2019 +0430

    create a bug

commit ab176fa8a282a74e6badfc285c0986bc66ee6b7d (origin/master, origin/HEAD)
Author: Ghasem Ramezani <g1999ramezani@gmail.com>
Date:   Sat May 4 10:40:32 2019 +0430

    make `top` to be 0 at first of parsing() function
    and make class storage of listArgs to be `extern`
    and getOption() function return "NULL" on Failure.

خب فهمیدم که آخرین تغییرم با عنوان create a bug ثبت شده، حالا باید از شناسه‌اش استفاده کنم.

۲- تغییراتی که در آن گزارش ثبت شده است را مشاهده می‌کنم. :

$> git show bb513a5f9ec429222de03afa690e7fa5d2fbdf6e
commit bb513a5f9ec429222de03afa690e7fa5d2fbdf6e (HEAD -> master)
Author: Ghasem Ramezani <g1999ramezani@gmail.com>
Date:   Sun May 5 00:05:22 2019 +0430

    create a bug

diff --git a/source/arg.c b/source/arg.c
index c776ff2..a75c91d 100644
--- a/source/arg.c
+++ b/source/arg.c
@@ -7,7 +7,6 @@ unsigned top=0;
 struct ARGS listArgs[MAX_ARG];

 void parsing(int argc, char **argv){
-    top = 0;
     for (unsigned i=1; i < (unsigned)argc; i+=2){
         listArgs[top].name    = argv[i];
         listArgs[top].value   = argv[i+1];

 

دیدی به چه سادگی توانستیم تغییری که دادیم را پیدا کنیم ؟ البته این انتهای ماجرا نیست ! الآن که متوجه شدیم در کدام گزارش‌ما خراب‌کاری کردیم؛ کافیه که تغییرات را به گزارش قبل از خراب‌کاری برگردانیم :

$> git reset --hard ab176fa8a282a74e6badfc285c0986bc66ee6b7d

البته قابل ذکره که ما اینجا تنها داخل این گزارش فقط یک تغییر داشتیم، مسلماً کار می‌تونه کمی پیچیده‌تر بشه اگه تغییرات زیاد باشن، که همیشه هستن ?.

 

چگونه با گیت (git) کار کنیم ؟

بسیار ساده، مسلماً اوّل نیاز دارید که این برنامه را نصب کنید. این برنامه به طور پیش‌فرض در سیستم‌عاملتون نصب نیست. کافیه که از مدیربستهٔ سیستم‌عاملتون کمک بگیرید، مثلاً برای Debian - Ubuntu - Ubuntu Mint به این‌صورت کار تمام می‌شود :

$ apt install git

حالا بعد از نصب، نیاز دارید که مشخصاتتان را ثبت کنید، دقت کنید که تمام توضیحاتی که بنده می‌دهم را می‌توانید به‌صورت کامل‌تر از سایت گیت (git) دنبال کنید.

$> git config --global user.name "Ghasem Ramezani"
$> git config --global user.email "g1999ramezani@gmail.com"
$> git config --global core.editor emacs

دو مورد اوّل که واضح هستن، امّا مورد آخر دل‌بخواه خودتان هست، زمانی‌که نیاز باشه گیت (git) ویرایشگرمتنی را جهت ویرایش‌باز بکند باید بداند که کدام ویرایشگر مورد علاقهٔ شماست. می‌توانید هر برنامه‌ای را قرار بدهید. امّا دقت کنید که بهترین ویرایشگر‌ها می‌توانند Vim, Emacs, Notepad++ باشند؛ فایل این تنظیمات را می‌توانید از این مسیرها دنبال کنید :

User Space : ~/.gitconfig
System Wide: /etc/gitconfig

 

ساخت مخازن (repository)

خب حالا که نصب/پیکربندی انجام شد، کافیه که مخزن (repository) خودمان را راه‌اندازی کنیم. یک پروژهٔ جدید درست کنید و گیت (git) را مقداردهی (Initialize) کنید :

$> mkdir project ; cd project
$> git init

ما یک دایرکتوری به اسم project درست کردیم، و مخزن (repository) خودمان را با دستور git init راه‌اندازی کردیم، یک سری فایل‌هایی گیت (git) برای ما داخل آن دایرکتوری با اسم .git درست کرده.

 

تغییراتی‌که نیاز رو انجام میدیم، مثلاً در وهلهٔ اوّل دایرکتوری‌ها و فایل‌های پروژه را راه‌اندازی می‌کنیم :

$> mkdir header source build object
$> touch header/arg.h source/arg.c Makefile
$> tree
.
├── build
├── Makefile
├── header
│   └── arg.c
├── object
└── source
    └── arg.h

4 directories, 3 files
$>

اگه درمورد Makefile نمی‌دانید، می‌توانید از اینجا با GNU Make و Makefile آشنا بشید.

الآن بد نیست که خروجی دستور git status را ببینیم تا توضیحاتی در این‌باره بدیم (این دستور، وضعیت‌جاری مخزنمان را نشان می‌دهد) :

$> git status
On branch master

No commits yet

Untracked files:
  (use "git add <file>..." to include in what will be committed)

	Makefile
	header/
	source/

nothing added to commit but untracked files present (use "git add" to track)

عکس زیر را مشاهده‌کنید تا توضیح‌بهتری بدم :

lifecycle.png.bd9237a37943ac5f22236127e075d447.png

فایل‌های شما داخل گیت (git) دارای حالات‌های مختلفی‌هستن، به طورکلّی یا شناخته‌شدن‌اند (tracked) یا ناشناخته‌اند (untracked)؛ فایل‌ها/دایرکتوری‌هایی که ما بعد از مقدار‌‌دهی مخزن‌مان ساختیم، در حالت ناشناخته (untracked) هستند. که خود گیت (git) هم همین‌را به ما گفته‌است :

Untracked files:
  (use "git add <file>..." to include in what will be committed)

برای اینکه شناخته‌شده (tracked) بشند، باید آنها را به صحنه (stage) ببریم. برای اینکار خود گیت گفته‌است که باید چه کرد که می‌توانیم به دوصورت انجام دهیم :

$> git add Makefile source header
$> git add -A

خب  حالا دوباره خروجی git status را نگاه می‌کنیم :

$> git status
On branch master

No commits yet

Changes to be committed:
  (use "git rm --cached <file>..." to unstage)

	new file:   Makefile
	new file:   header/arg.c
	new file:   source/arg.h

الآن فایل‌های ما به stage رفتند، و آمادهٔ این‌هستند که گزارش‌ (commit) بشوند. دقت کنید که Git از خِیر دایرکتوری‌های خالی می‌گذرد.

حالا کافیه‌که ما تغییراتی که دادیم را گزارش کنیم، که با دستور git commit به دوصورت انجام می‌شود :

$> git commit
$> git commit -m "My Message"

در حالت‌اوّل، گیت ادیتور پیش‌فرضتان را باز می‌کند و از شما می‌خواهد که یک توضیح‌کوتاه درمورد تغییراتی‌که داده‌اید بنویسید، در حالت‌دوّم، شما مستقیم توضیح‌کوتاه خود را وارد می‌کنید. حال دوباره برگردیم و خروجی دستور git status را ببینیم :

$>  git status
On branch master
nothing to commit, working tree clean

خیلی‌هم عالی، این نشان دهندهٔ این‌است که ماهیچ فایل ناشناخته (َUntracked) یا دستکاری‌شده (Modified) یا درصحنه (Stage) نداریم. هنگامی‌که تغییراتی را در فایل‌های شناخته‌شده (Tracked) بدید، آن فایل از حالت دستکاری‌نشده (Unmodified) به حالت دستکاری‌شده (Modified) درمیاید، که نیاز است شما تغییرات را درصورت‌نیاز وارد صحنه (Stage) کنید و بعد گزارش‌کنید (Commit).

حال تغییراتی‌اعمال می‌کنیم، و مراحل‌مورد نیاز تا درصحنه (Stage) بردن‌فایل‌ها انجام می‌دهیم :

$> git add -A
$> git status
On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

	modified:   Makefile
	modified:   header/arg.h
	modified:   source/arg.c

امّا شاید شما نخواید که مثلاً Makefile گزارش‌‍ش با مابقیه فایل‌ها یکی باشه، و نیاز دارید که از Stage بیرون بیاریدش؛ دقّت کنید خود Git هم راهنمایی‌ کرده که باید چه‌کار کرد :

$> git reset HEAD Makefile
$> git status
On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

	modified:   header/arg.h
	modified:   source/arg.c

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

	modified:   Makefile

و حالا می‌توانید به راحتی گزارش فایل‌های خودتان را برای header/arg.h و source/arg.c بنویسید :

$> git commit -m "Done With Print() Function"

 

فایل .gitignore

بیاید تا make را اجرا کنیم (درصورتی‌که با GNU Make آشنا نیستید، برای آشنایی این‌قسمت را مطالعه کنید) :

$> git status
On branch master
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

	modified:   Makefile

Untracked files:
  (use "git add <file>..." to include in what will be committed)

	build/
	object/

no changes added to commit (use "git add" and/or "git commit -a")

اینجا دایرکتوری‌های build و object هم اضافه شدند،امّا ما نیازی نداریم که Git این دایرکتوری‌ها را مدیریت کند، پس کافیه که یک فایل به اسم .gitignore‌ درست کنیم. و فایل‌ها و دایرکتوری‌هایی که نمی‌خواهیم Git آنها را دنبال کند را در آن ذکر کنیم :

$> echo -e "/build/*\n/object/*" > .gitignore
$> cat .gitignore 
/build/*
/object/


$> git status
On branch master
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

	modified:   Makefile

Untracked files:
  (use "git add <file>..." to include in what will be committed)

	.gitignore

no changes added to commit (use "git add" and/or "git commit -a")

و مشاهده می‌کنید که دیگر Git اخطاری برای دایرکتوری‌های build‌ و object نداد.

 

خب دوستان،امیدوارم دلیل اهمیّت Git و کلاً برنامه‌های کنترل‌ورژن را درک‌کرده باشید؛ امّا شرمنده، دنیای Git بزرگ‌تر از آنچه هست که من بخوام خلاصه‌ای از هر قسمت را در یک پست باز‌گو کنم؛ شدیداً پیشنهاد می‌کنم که این‌کتاب را برای فراگیری هرچه بهتر Git‌ بخوانید.

- موفق و پیروز باشید. ?

قاسم رمضانی منش

خب ! Build System چیست ؟

تمام برنامه‌هایی که می‌نویسیم، معمولاً یک main.c دارند که نقطهٔ‌شروع (start point) برنامهٔ‌ما هست. آیا همیشه همین یک فایله ؟ آیا همیشه نیازه که به یک‌صورت برنامه‌ را کامپایل کنیم ؟ خب مسلماً جواب "نه" هست. چرا که ممکنه برنامهٔ شما دارای ده‌ها فایل داشته‌باشه، و نیاز داشته‌باشید که هر فایل رو به صورت‌خاصی با فلگ‌های خاصی کامپایل‌کنید. اینجاس که "بیلد سیستم‌"ها وارد کار میشوند. 

به احتمال زیاد نمونه‌های زیادی مشاهده کردید که وقتی یک سورسی‌را (source) از مخازن آنلاین گیت، مثل گیت‌هاب یا گیت‌لب دریافت می‌کنید در فایل‌ راهنما (README.md) در بخش Build نوشته که وارد دایرکتوری بشید و دستور make و بعد make install را وارد کنید، دقیقاً کاری که می‌کنید اینکه برنامهٔ GNU Make را صدا می‌زنید که فایل تنظیمات رو از دایرکتوری جاری بخواند و دستورات تعیین شده رو انجام بده، این دستورات در فایلی به نام Makefile‌ نوشته میشود.

 

نصب کردن GNU Make

این برنامه معمولاً روی تمام سیستم‌عامل‌های معقول مثل GNU/Linux یا اقوام BSD نصب هست، درصورتی‌که نبود می‌توانید با استفاده از مدیربسته‌ٔ سیستم‌عاملتون اقدام به نصب کنید، مثلاً برای نصب روی سیستم‌عامل Debian - Ubuntu - Ubuntu Mint می‌توانید به این‌صورت عمل کنید :

$> apt install make

 

چه کنیم با GNU Make ؟

اوّل از همه باید یک برنامه‌ای داشته‌باشیم که بخوایم براش Build System تعیین کنیم و دستورات Makefile‍‍ش رو بنویسیم. یک نمونهٔ ساده کد چند تکه‌ای را می‌توانید از این‌قسمت دریافت کنید. ما سه فایل arg.c/arg.h و main.c را به این‌صورت داریم (یک ساختار معقول) :

.
├── build
├── obj
└── src
    ├── arg.c
    ├── arg.h
    └── main.c

خب حالا ما باید Makefile خودمان را داخل دایرکتوری ریشه درست کنیم، قبلاً هم گفتم : "برنامهٔ GNU Make به دنبال فایلی به اسم Makefile یا GNUmakefile یا makefile می‌گرده". در Makefile می‌توانیم‌ما قوانین (rule) برای ساخته شدن چیزی و متغیر‌هایی تعریف کنیم. اینجا من توضیحات خلاصه‌ای را می‌گویم، باقی‌ماندهٔ مطالب را باید از مستندات‌رسمی GNU Make یا راهنمای سریع دنبال کنید. هر قوانین‌‍ای که تعریف می‌کنیم دارای این ساختار هست :

نیازها : هدف‌ها
    دستورات

مثلاً ما می‌خواهیم که برنامهٔ‌کامپایل شدهٔ‌مان، با اسم args در دایرکتوری build/ قرار بگیره. اینجا "هدف"ما میشه build/args و نیازما هم فایل‌های کامپایل‌ شدهٔ arg.c و main.c هست. اوه ! یک هدف دیگه‌هم پیدا شد؛ الآن هدف دوّم‌ما فایل‌های کامپایل شدهٔ obj/arg.o و obj/main.o هست و نیازمان هم سورس‌های این فایل‌ها یعنی src/arg.h و src/arg.c و src/main.c.

خب خیلی زیاد شدن، بهتره که از آخر شروع کنیم و نیازهایمان را برطرف کنیم، اوّلین نیاز فایل‌های کامپایل‌شده هستن :

obj/main.o obj/arg.o : src/main.c src/arg.c src/arg.h
    gcc -c -o obj/main.o src/main.c
    gcc -c -o obj/arg.o src/arg.o

*  نکته : سعی نکنید دستورات Makefile را از منطقهٔ کد کپی نکنید، کمی تلاش کنید و بنویسید.

خب قبول دارم خیلی زیاد و زشت شد، بیاید این قانون (rule) را به دو تیکه قسمت کنیم :

obj/arg.o : src/arg.c src/arg.h
	gcc -c -o obj/arg.o src/arg.c

obj/main.o : src/main.c
	gcc -c -o obj/main.o src/main.c

اگر تا انتها متن ادامه بدید حتماً کوتاه‌ترم خواهد شد :).

خب؛ object fileها یا همان فایل‌های کامپایل شده‌‍‌یمان را به دست‌آوردیم. حالا باید قانون (rule) نیاز اوّلمان را بنویسیم، چه چیزی نیاز داشتیم ‌؟ فایل کامپایل شدهٔ build/args که نیاز به object fileها داشت، حالا object fileها را داریم و باید نیاز هدفمان را برطرف کنیم :

build/args : obj/main.o obj/arg.o
	gcc -o build/args obj/main.o obj/arg.o

obj/arg.o : src/arg.c src/arg.h
	gcc -c -o obj/arg.o src/arg.c

obj/main.o : src/main.c
	gcc -c -o obj/main.o src/main.c

تمام شد. ما دستورات Build System خودمان را به زبان برنامهٔ GNU Make نوشتیم؛ حالا کافیه که فقط وارد دایرکتوری‌ای که فایل Makefile هست بشیم و از ترمینال برنامهٔ make را فراخوانی کنیم :

$> make
gcc -c -o obj/main.o src/main.c
gcc -c -o obj/arg.o src/arg.c
gcc -o build/args obj/main.o obj/arg.o

حالا می‌توانیم برنامهٔ خودمان را اجرا کنیم :

$> build/args -name Ghasem -family Ramezani
Input Name   is [Ghasem]
Input Family is [Ramezani]

دقّت کرده باشید ما توی نوشتن Makefileمان نیازمندی‌هارو یکی بالاتر از دیگری نوشتیم. چرا ؟ به خاطر اینکه GNU Make میاد از اوّل فایل شروع می‌کنه و قوانین (rules)ها را اجرا می‌کنه. بزارید با یک مثال نشان بدم. Makefile زیر را مدنظرتون داشته‌باشید :

obj/arg.o : src/arg.c src/arg.h
	gcc -c -o obj/arg.o src/arg.c

obj/main.o : src/main.c
	gcc -c -o obj/main.o src/main.c

build/args : obj/main.o obj/arg.o
	gcc -o build/args obj/main.o obj/arg.o

ما نیاز اصلی خودمان را آخرین قانون (rule) نوشتیم. حالا برنامهٔ make را اجرا می‌کنیم تا رفتارَش را بهتر متوجه بشیم :

$> make
gcc -c -o obj/arg.o src/arg.c

دیدید ؟ خیلی ساده برخورد کرد، اوّلین قانون (rule) را نگاه کرد تنها نیازمندیش فایل‌های src/arg.c و src/arg.v بودن که وابسته به چیزی نبودند و هدفشان را تأمین کردند. اگر بخواهیم باقی قوانین (rules) را فراخوانی کنیم، باید صراحتاً مشخص کنیم :

$> make obj/main.o
gcc -c -o obj/main.o src/main.c


$> make build/args 
gcc -o build/args obj/main.o obj/arg.o

خب دیگه امیدوارم دلیل اینکه‌ما نیازمندی اصلیه خودمان را اوّلین قانون (rule) قرار دادیم را متوجه شده باشید. وقتی make به نیازمندیه obj/arg.o و obj/main.o برای تأمین build/args برمی‌خوره ادامهٔ قوانین را پیمایش می‌کنه تا نیازمندی‌ها را برطرف کنه. (اگر گیج شدید احتمالاً، پیشنهاد می‌کنم همین‌ موارد را روی کاغذ کشیده و قسمت : نیازمندی‌ها و هدف‌ها و دستورات هر قانون را مشخص کنید.)

می‌توانیم قوانینی (rules) تعریف کنیم برای کارهای خاصی، مثلاً همان make install، یعنی قانون install را فراخوانی کن؛ حالا ما قانون clean را برای حذف کردن فایل‌های کامپایل‌شده می‌نویسیم :

clean :
	yes | rm -vf build/* obj/*

البته باید در اینجا نکته‌ای را هم حواسمان باشد، باید به GNU Make بگوییم که قانون clean ، یک قانون الکی‌هست، و با یک "هدف" اشتباه نشود. به این‌صورت قانون را ویرایش می‌کنیم :

.PHONY : clean
clean :
	yes | rm -vf build/* obj/*

نگرانی‌ای هم دربارهٔ Wildcard ها نداشته‌باشید، GNU Make دستتون را باز گذاشته :).

متغیرها در GNU Make

مسلماً هرجا سخنی از متغیر‌است، سر و کلهٔ راحتی‌کار (و تا حدودی پیچیدگی) پیدا می‌شود. ما می‌توانیم متغیرهم داخل Makefile خودمان داشته‌باشیم. مثلاً فرض کنید که نیاز دارید تمام سورس‌کدها با کامپایل clang و سطح‌بهینه‌سازیه 3 کامپایل بشند. نیازی نیست‌که هربار اینارو تایپ کنیم. کافیه براشون متغیرتعریف کنیم :

CC = clang
OP = -O3 
OBJECT = obj/main.o obj/arg.o
ARGS   = src/arg.c src/arg.h

build/args : $(OBJECT)
	$(CC) $(OP) -o build/args $(OBJECT)

obj/arg.o : $(ARGS)
	$(CC) $(OP) -c -o obj/arg.o src/arg.c

obj/main.o : src/main.c
	$(CC) $(OP) -c -o obj/main.o src/main.c

clean :
	yes | rm -vf build/* obj/*

متغیرهای به خصوصی نیز در GNU Make تعریف شده‌اند که می‌توانند کار مارا بسیار راحت‌تر کنند،‌ برای مثال می‌توانیم قانون object file‌ها را به اینصورت بازنویسی کنیم :

obj/%.o : src/%.c
	$(CC) $(OP) -c -o $@ $?

برای اطلاعات بیشتر به راهنمای‌سریع GNU Make مراجعه کنید.

 

یادداشت‌ها یا Code Comments

برای استفاده از قابلیت Comment گذاری در کد، کافیه که اوّل خط خودتون از کاراکتر # استفاده کنید.

 

 

خب دوستان، سعی کردم کلیّات مبحث را بگم؛ ابزار Make قابلیت‌های بسیار زیادی داره که حتماً باید خودتون مطالعه کنید. مثلاً خواستید Makefile شما یک Makefile دیگه را صدا بزنه، یا حتیٰ دستورات شرطی اجرا بکند و یا از همه مهم‌تر بر اساس معماری پلتفرم شما عملیات کامپایل را انجام بده و ... .

- موفق‌وپیروز باشید. ?

مهدی کرامتی

مکانیسم Role Management در ASP.NET Identity کمبودهای بسیاری دارد، از جمله اینکه جهت مشخص کردن سطح دسترسی، نام Role ها میبایست در صفت Authorize نوشته شده و Hard Code شود. این بدان معناست که برای انجام هر گونه تغییر در کنترل دسترسی مجبور خواهید بود سورس برنامه تان را دستکاری کنید. همچنین، عدم وجود امکانی برای ویرایش کاربران و Role ها، کار کردن با این مکانیسم را مشکل می کند.

در راستای حل این مشکلات و کمبودها، ما یک کیت توسعه نرم افزار (SDK) به نام Permission Control System طراحی و ارائه کرده ایم. با استفاده از این SDK نیاز نیست هیچ چیزی را در برنامه جهت مدیریت دسترسی Hard Code کنید، تمام امکانات مدیریتی Dynamic است!

پس از فعال کردن این SDK، هر Controller یا Action ای که به پروژه تان اضافه کنید پس از اجرای بعدی برنامه به صورت خودکار در صفحه Role Manager ظاهر خواهد شد و می توانید انتخاب کنید که کدام گروه از کاربران به چه Action ای بر حسب Role شان دسترسی خواهند داشت.

آموزش کامل ویدئویی این SDK نیز با تشریح کد آن به صورت خط به خط برنامه در همین پکیج ارائه شده است. علاوه بر آموزش اجزاء SDK، موارد زیر نیز در آموزش ویدئویی ارائه شده است:

  • آموزش کامل ASP.NET Identity با تشریح کامل کدها
  • روش تغییر نام جداول و ستونهای ایجاد شده توسط مکانیسم Identity
  • طراحی و پیاده سازی سیستم رمز یک بار مصرف (OTP) جهت لاگین به سیستم
  • ایجاد و استفاده از Attribute ها
  • ایجاد و استفاده از Global Filter ها برای کنترل دسترسی
  • ایجاد و استفاده از HTML Helper ها

لینک صفحه مربوط به SDK و آموزش ویدئویی

https://github.com/delphiassistant/permission_control_system

 

لطفا نظرات و پیشنهادات خود درباره بهبود این SDK را در پاسخ این نوشته ارسال کنید.

 

با تقدیم احترام،
مهدی کرامتی.

کامبیز اسدزاده

کامپایلر Cling یک مترجم تعاملی برای سی‌پلاس‌پلاس است، این مترجم تحت بالاترین کتابخانه‌های Clang و LLVM ساخته شده است. در واقع از آن‌جایی که کامپایلر Clang از آخرین ویژگی‌ها و استاندارد‌های زبان سی‌پلاس‌پلاس پشتیبانی می‌کند، Cling اجازه می‌دهد تا توسعه‌دهندگان اسکریپت‌های خود را با استفاده از C و C++ بنویسند. اگر شما به طور مستقیم مترجم را اجرا کنید، یک محیط زنده برای آغاز برنامه نویسی با سی‌پلاس‌پلاس را خواهید داشت که به عنوان بخشی از استاندارد نحو سی و سی‌پلاس‌پلاس به شمار می‌آید. همچنین می‌توانید دیگر دستورات را با نقطهٔ "." آغاز در اختیار داشته باشید.

وقتی از مترجم تعاملی استفاده می‌کنید، می‌توانید کد زیر را بنویسید:

#include <stdio.h>
printf("hello world\n");

همانطور که می‌بینید نیازی نیست تا در مورد حوزهٔ دامنه‌ها نگران باشید؛ کافی است شما تابع مورد نظر خود را صدا بزنید.

اگر قصد شما این است که از Cling به عنوان یک مترجم برای ساخت اسکریپت‌ها استفاده کنید، باید همه چیز را در داخل یک تابع قرار دهید.چرا که نقطهٔ ورود به اسکریپت به طور پیش‌فرض همانند نام فایل می‌باشد. می‌توان آن را برای صدا زدن دیگر توابع سفارشی سازی کرد. بنابراین مثال قبل می‌توانید به شکل زیر تغییر کند:

#include <stdio.h>                                                                               
                                                                                                        
void _01_hello_world() {                                                                               
    printf("foo\n");                                                                                   
}

یک نسخهٔ دیگر در قالب سی‌پلاس‌پلاس

#include <iostream>                                                                               
 
void _02_hello_world()
{
    std::cout << "Hello world" << std::endl;
}

مثال‌ها کاملاً ساده هستند، اما آن‌ها به شما نشان می‌دهند که چگونه باید شروع کنید.

در مورد کیوت چطور؟

#include <QtWidgets/qapplication.h>                                                                    
#include <QtWidgets/qpushbutton.h>                                                                     
                                                                                                        
void _03_basic_qt()                                                                                    
{                                                                                                      
    int argc = 0;                                                                                      
    QApplication app(argc, nullptr);                                                                   
                                                                                                        
    QPushButton button("Hello world");                                                                 
    QObject::connect(&button, &QPushButton::pressed, &app, &QApplication::quit);                       
    button.show();                                                                                     
                                                                                                        
    app.exec();                                                                                        
}

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

cling -I/usr/include/x86_64-linux-gnu/qt5 -fPIC -lQt5Widgets 03_basic_qt.cpp

شما می‌توانید Cling را برای خودتان بر اساس آن چیزی که برای اسکریپت خود نیاز دارید سفارشی سازی کنید. همچنین شما می‌توانید Cling را به عنوان یک کتابخانه در اپلیکیشن‌های خود آورده و از سی‌پلاس‌پلاس به عنوان زبان برنامه‌نویسی استفاده کنید. این پُست در آینده ادامه خواهد داشت. ?

کامبیز اسدزاده

دربارهٔ کامپایلر Zapcc

کامپایلر Zapcc یک کامپایلر بر پایه Clang است که با هدف کامپایل‌های سریعتر طراحی شده است. این کامپایلر با استفاده از حافظه نهان (Cache) و استفاده از معماری سرویس‌گیرنده-سرویس‌دهنده پیاده سازی شده است که یک کامپایلر مدرن و جدیدی به شما می‌آید که برای اهداف زیر ساخته شده است:

  • ساخت سریع: تسریع در جمع آوری‌های قابل توجه برای هدرهایی که دارای قالب‌های سنگین در سی پلاس پلاس می‌باشند مانند LLVM، WebKit، ScyllaDB
  • بر پایه Clang/LLVM: این کامپایلر بر پایخ Clang و اغلب بر ساس آخرین SVN به روز رسانی شده است.
  • پشتیبانی کامل از لینوکس: در حال حاضر این کامپایلر از لینوکس x64 و ویندوز x64 با MinGW-w64 به صورت آزمایشی پشتیبانی می‌کند.
  • جایگزینی: جایگزینی برای Clang و GCC و پشتیبانی از تمامی سیستم‌های ساخت (Build Systems) .
نقل قول

پس از مقدار دهی اولیه، کامپایل مجدد با استفاده از کامپایلر Zapcc تقریباً ۴۰ برابر سریعتر است.

مجوز‌ها

این پروژه منبع باز تحت مجوز LLVM از (University of Illinois/NCSA) می‌باشد.

ساخت (Building)

پیش نیازها و فرآیند ساخت همانند LLVM می‌باشد.

git clone https://github.com/yrnkrn/zapcc.git llvm
mkdir build
cd build
cmake -G Ninja -DCMAKE_BUILD_TYPE=Release -DLLVM_ENABLE_WARNINGS=OFF ../llvm
ninja

اجرا و آزمایش

ninja check-all

استفاده

نحو دستورات Zapcc همانند دستورات Clang می‌باشد.

از بین بردن سرور Zapcc

pkill zapcc

این دستور جهت از بین بردن سرور Zapcc برای آزاد سازی حافظه یا جایگزینی با سیستم تازه ساخته شده Zappc استفاده شود.

جهت اطلاعات بیشتر به این بخش مراجعه کنید.

لینک منبع بر روی گیت‌هاب

کامبیز اسدزاده

برای بسیاری از توسعه دهندگان نرم افزار کار کردن بدون کنترل نسخه غیر قابل تصور است. فواید کنترل و پیگیری تاریخچه تغییرات کدها برای درک کردن دنیای توسعه نرم افزار بسیار بالاست. با توجه به این نباید از نتایج به دست آمده از تحقیق انجام شده توسط DevOps که استفاده از تاریخه کدها بسیار بالاست شگفت زده شد.

اما پرسیدن در مورد کنترل نسخه دیتابیس موضوع دیگری است. تنها ۵۸ ٪ از کسانی که در این تحقیق شرکت کرده اند گفته اند که کنترل نسخه دیتابیسشان را رصد می‌کنند. البته به طریقی این قابل درک است که چرا کنترل نسخه برای مدت بسیار زیادی بر روی دیتابیس انجام نمی پذیرفت. اما اکنون زمان این رسیده است که دیگر تیم‌ها بتوانند بر روی دیتابیس کار کنند.

اگر شما هنوز کنترل نسخه برای دیتابیس خود انجام نداده اید ما در اینجا دلایلی آورده ایم که اینکار برای شما بسیار حیاطی می‌باشد:

به راحتی می‌توانید تغییرات کدها را با تیمتان به اشتراک بگذارید
کنارهم قرار دادن دیتابیس کد‌ها با سیستم کنترل نسخه کار کردن اعضای تیم بر روی کدهای دیتابیس و مسئولیت پذیری آن‌ها را بر روی کارهایشان بیشتر می‌کند. توانایی به اشتراک گذاردن مداوم و مدیریت تغییرات برای تیم های که در کنار هم کار نمی کنند بسیار حیاتی است. به وسیله  SQL Sourse Control   اعضای می توانند بر روی یک دیتابیس به اشتراک گذارده شده و یا هر کدام بر روی یک دیتابیس LOCAL که یک کپی از نسخه اصلی است کار کنند. با افزودن ویژگی‌هایی مانند object locking شما می می‌توانید از تداخل های احتمالی جلوگیری کنید و کار را بدون تداخل جلو ببرید.

از نحوه توسعه نمای بهتری به دست خواهید آورد:
سیستم کنترل نسخه برای شما یک نمای کلی از توسعه کلی کاری که انجام می‌دهید نشان می‌دهد. کنترل نسخه برای شما تاریخچه تغییرات را نشان می‌دهد و به راحتی با سیستم های کنترلی و پیگیری کار ‌می‌کند. به طور مثال SQL Source Control به شما اجازه همگام سازی وظایف دیتابیس را با Mircosoft Team Foundation Server work item ها می‌دهد و به وسیله آن به راحتی می توانید جریان کار را کنترل کنید.

به شما توانایی Rollback و بازگشتن به ورژن قبلی دیتابیس را می‌دهد.
در حالی که شما همواره یک استراتژی Backup مناسب دارید. استفاده از کنترل نسخه برای دیتابیس یک مکانیزم برای back up  گرفتن از SQL کدهای شما در اختیارتان قرار می‌دهد. با استفاده از SQL Source Control کار کردن و بازگرداندن نسخه های قبلی بسیار آسان و ساده هستند.

حسابرسی و خوانایی کدها را ساده‌تر می کند
تغییر ورژن کنترل،‌اولین قدم برای آماده سازی خوانایی کدها و یک قدم ضروری برای بهتر کردن حسابرسی و مدیریت ریسک می‌باشد. حسابرسی صحیح نیازمند یک سازماندهی برای کلیه تغییران بر روی دیتابیس می‌باشد و آن نیازمند جزییان برای دسترسی است. با استفاده از SQL Source Control شما می‌توانید نسخه کامل تاریخچه دیتابیس و یا database object را دسترسی داشته باشید و ببینید که چه کسی تغییرات را ایجاد کرده است، چه زمانی آنها را انجام داده است و چرا.

پایه ریزی برای Database Automation
داشتن یک نسخه از دیتابیس مدیریت تغییرات را ساده تر می‌کند. پردازش‌های پیچیده اتوماتیک‌تر و تکرارپذیرتر می‌شوند و تغییرات نیز قابل پیش‌بینی می‌گردند. استفاده از کدی که در داخل SQL Source Control به عنوان پایه ساختن و تست های DLM Automation را اتوماتیک می‌کند و این بدین معنی است که مسائل سریع‌تر پیدا می‌شوند و کدی با کیفیت بالاتر تولید و منتشر می‌گردد.

همگام‌سازی دیتابیس و تغییرات کد‌های نرم‌افزار
داشتن یک دیتابیس با کنترل نسخه دقیقا در کنار اپلیکیشن تغییرات کد‌های دیتاییس و اپلیکیشن را همگام می‌کند. شما همواره خواهید دانست که چه نسخه‌ای از کد بر روی چه ورژنی از نرم‌افزار قرار داده شده است. این به شما کمک می‌کند تا انجام پروژه به صورت تیمی را بسیار ساده‌تر کنید، اثربخشی کار را بالاتر ببرید و مشکلات را سریع تر برطرف کنید. SQL Source Control که به سیستم های کنترل نسخه مانند TFS, Git, Subversion متصل شود تغییرات کدها را ذخیره می‌کند.

خلاصه:
در حالی که این مساله صحیح است که کنترل نسخه همواره به دست نمی‌اید، اما در دسترس بودن ابزارهایی مانند SQL Source Control به این معنی است که دیگر دلیلی برای بعضی از شرکت‌ها که این کار انجام می‌دهند نباشد. اگر شما یکی از ۴۲ ٪ هستید که تا اکنون این کار برای دیتابیس خودتان انجام نداده اید، شاید این ۶ دلیل بالا بتواند نظر شما را عوض کند.

کامبیز اسدزاده

برنامه‌نویس تنها در این عنوان خلاصه نمی‌شود و لازم است بدانید که برنامه‌نویسان در چند دسته متفاوت وجود دارند که برخی از آن ها به صورت Back-End و برخی Front-End فعالیت می‌کنند. در کل به کسانی که توانایی برنامه‌نویس در بخش Back-End را دارند به آن‌ها Back-End Developer می‌گویند. همچنین برنامه‌نویسانی که توانایی توسعه در بخش طراحی رابط‌کاربری و تجربه‌کاربری را با عنوان Front-End دارند Front-End Developer می‌گویند.

در نظر داشته باشید که توسعه‌دهندگان و طراحان بخش تجربه‌‌کاربری (UX) و رابط‌کاربری (UI) خود وظایفی در سمت طراحی یک محصول را دارند که به خودی خود می‌توانند به عنوان توسعه‌دهندهٔ فرانت‌اِند محسوب شوند اما ممکن است زمینهٔ اجرایی آن‌ها با محیط‌های توسعه که شامل کد‌نویسی هستند نباشد! بنابراین شاخه‌ای از حوزهٔ توسعه در نرم‌افزار کامپیوتر وجود دارد که می‌تواند با ترکیب دانش طراحی و کد‌نویسی و تسلط کامل بر این دو حوزه به صورت ترکیبی با دانش و توانایی بسیار بالا عنوان شود که به آن فول‌اِستک می‌گویند.

البته فول‌اِستک ابعاد مختلفِ خود را دارد، برای مثال ممکن است یک توسعه‌دهندهٔ فول‌اِستک تنها در پلتفرم اندروید توانایی طراحی و کد‌نویسی را به صورت همزمان و بدون نیاز به یار تیمی خود داشته باشد. اما در اصل توسعه‌دهنده‌های با تجربه با سابقهٔ بالا که توانایی مدیریتی پروژه و توسعهٔ آن‌ها را دارند از نوع فول‌اِستک تمام عیار محسوب می‌شوند که در ادامه به ویژگی‌های آن‌ها اشاره شده است.

نقل قول

عنوان Full-Stack Developer به معنای واقعی شامل افرادی می‌شود که واقعا استاد برنامه‌نویسی هستند. این سری افراد یک توسعه‌دهنده فول استک، یک گیک به معنای واقعی کلمه هستند.

یک برنامه‌نویس حرفه‌ای یا همان فول‌اِستک می‌بایست مهارت‌های زیر را داشته باشد:

  • مسلط به زبان‌های برنامه‌نویسی پایه
  • آشنایی با UX و UI و مباحث مرتبط با هر یک از آن‌ها
  • مدیریت پروژه بر روی پلتفرم‌های مختلف
  • توانایی کنترل کیفیت محصول
  • توانایی کار با انواع فناوری‌ها و کتابخانه‌ها
  • توانایی کار با انواع دیتابیس و مدیریت آن‌ها
  • هک و امنیت
  • بهینه سازی موتور‌های جستجو
  • آشنایی و توانایی درک و مدیریت کامپایلر‌ها و مفسر‌ها
  • درک نیاز‌های کاربران در محصول (UX)
  • آشنایی با سیستم عامل‌های مختلف
  • آشنایی و توانایی تولید محصول به صورت چند-سکویی (Cross-Platform)
  • آشنایی با شبکه و پیکربندی آن برای محصول
  • آشنایی با مدیریت سرور و هاستینگ
  • آشنایی با سیستم‌های مدیریتی و مجازی مانند VM
  • آشنایی با سخت افزار
  • آشنایی با رابط های برنامه نویسی API‌ها
  • آشنایی با انواع محیط‌های توسعه
  • و موارد دیگر که در یک پروژه از صفر تا صد می‌توان به آن‌ها نیاز پیدا کرد.
  •  برنامه‌نویسان Full-Stack Developer به تنهایی می‌تواند درتولید و توسعه یک محصول موثر باشد و زمانی که با مشکلی مواجه شوند نمی‌گوید من آن را بلد نیستم، بلکه حتماً آن را حل خواهند کرد.

به طور کلی کسب مهارت در سطح بالا در حد یک توسعه‌‌ دهنده فول‌اِستک بسیار سخت است اما نباید بگوییم که غیر ممکن است، در صورتی که چنین تعریفی برای یک توسعه‌دهندهٔ فول‌استک در نظر بگیریم، بدون اغراق باید گفت تعداد اندکی از این برنامه‌نویسان موجود است که بتوانیم چنین لقبی را به آن‌ها اختصاص بدهیم بنابراین چنین برنامه‌نویسانی بسیار ارزشمند هستند لذا به خوبی می‌دانند یک نرم افزار چگونه طراحی‌ می‌شود و توانایی این را دارند از صفر تا صد یک نرم‌افزار را طراحی و روانه بازار کنند. علاوه بر این توسعه دهنده Full-Stack کسی است که واژگانی مانند نبود، نمی‌شه، امکان نداره، نمی‌توم، کار من نیست و ... را بر زبان نمی‌آورند و اگر هم چیزی را ندانند تمام تلاش خود را می‌کنند تا بدون نیاز به کمک شخصی دیگر آن را حل کنند. این نوع توسعه‌‌دهنده‌ها بسیار با ارزش و مهم هستند، و نکته جالب اینجاست که آن‌ها سال‌ها تلاش کرده‌اند و مسلماً به تنهایی صاحب کسب‌و‌کار خود بوده و در انتخاب اول برای کسی کار نمی‌کنند.

برای توسعه دهندهٔ فول‌اِستک فرقی نمی‌کند محصول تحت چه پلتفرمی باشد، او می‌تواند تحت دسکتاپ، وب، موبایل و دیگر پلتفرم ها آن را تولید کند.

کامبیز اسدزاده

شما برنامه نویس هستید یا توسعه‌دهنده؟ آیا تا به حال فکر کرده‌اید که چه نوع عنوانی متناسب با مهارت‌های شماست؟

  • من یک (توسعه دهنده فول-استک هستم)

  • من یک (برنامه‌نویس هستم)

  • من یک (توسعه‌دهندهٔ وب هستم)

  • من یک (توسعه‌دهندهٔ فرانت اند هستم)

  • من یک (توسعه دهندهٔ iOS هستم)

  • و عناوین بسیار زیاد من در آوردی دیگر که ممکن است منظور دقیق از مهارت‌های شما را به درستی مطرح نکنند!

من می‌خوام در رابطه با اصطلاحاتی صحبت کنم که شاید بسیاری از علاقه‌مندان از استفادهٔ به جا از آن‌ها بی‌خبر هستند. داستان این مقاله از اینجا شروع می‌شه که امروزه عناوین متعددی در رشتهٔ مهندسی کامپیوتر به خصوص گرایش نرم‌افزار و (برنامه‌نویسی) به چشم می‌خوره که ممکنه باعث سردرگمی بشه. نکته اصلی اینجاست که ما به عنوان صاحب عنوان تخصصی خود باید بدانیم که دارای چه مهارت‌هایی هستیم و در دنیای فناوری تحت چه عنوانی باید خود را در زمینه تخصصی معرفی کنیم. 

photo-1522071820081-009f0129c71c.jpeg

تفاوت بین کدنویس، برنامه‌نویس و توسعه‌دهنده

در این پست قصد داریم با اصطلاحاتی آشنا شویم که ممکن برای اکثر افراد غیرمتخصص و گاه متخصص نیز سوال باشد که این عنوان‌ها (کدنویس، برنامه نویس و توسعه‌دهنده) به چه کسانی با چه تخصص‌های گفته می‌شود. ما به عنوان صاحب “عنوان تخصص” خود باید بدانیم که دارای چه مهارت‌هایی هستیم و در دنیای فناوری تحت چه عنوانی باید خود را در زمینه تخصصی معرفی کنیم.

در این مقاله تصمیم گرفتم تفاوت‌های بین کدنویس (Coder)، برنامه‌نویس (Programmer) و توسعه‌دهنده (Developer) را مشخص کنم. بنابراین به ترتیب عناوین هر یک از آن‌ها را توضیح و در رابطه با نکات مهم متمایز کنندهٔ آن‌ها اشاره خواهیم کرد.

1.کدنویس (Coder)

کد نویس به کسی گفته می‌شود که می‌تواند بدون داشتن مهارت خاص یا حتی رشته مرتبط کدنویسی کند و نیاز به دانش تخصصی و واقعی علوم کامپیوتری ندارد. معمولاً کسانی که دارای تخصص دیگری هستند اما آشنا به منطق برنامه‌نویسی نیستند کُدِر می‌گویند. برای مثال تغییر دادن و یا ویرایش کد‌های از قبل نوشته شده و حتی ایجاد نمونه‌ای از کدهایی موجود به صورت (کپی) که می‌تواند نتیجه‌ای به صورت کار بر روی یک سیستم نرم‌افزاری بر روی وب مانند WordPress یا غیره شود که با کمی تغییرات بر اساس نیاز پروژه خود را به صورت نه چندان حرفه‌ای ایجاد و توسعه نمایند. اصطلاح درست این نوع اشخاص کُدر می‌باشد.

2.برنامه‌نویس (Programmer)

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

برنامه‌نویس می‌داند در کجا باید از چه نوع دستورات و توابعی استفاده کند تا کدِ نهایی او نتیجه‌ای ایجاد کند که از آن انتظار می‌رود. یک برنامه‌نویس توانایی این را دارد که کُدهای نوشته شده توسط دیگر برنامه‌نویسان را بخواند، درک کند و حتی آن‌ها را ویرایش کند.

توجه داشته باشید که یک برنامه‌نویس توانایی کنترل و مدیریت کردن بخش‌های بسیار پیچیدهٔ یک محصول را ندارد. برای مثال اگر قرار است پروژه‌ای را طراحی و توسعه نمایید برنامه‌نویس بخش بَک-اِند توانایی مدیریت بخش فرانت-اند (رابط‌کاربری) را ندارد و برعکس! بنابراین برنامه‌نویسان تنها کُدهایی را می‌نویسند که قرار است در بخش مورد نیاز عملیاتی را انجام دهند و کاری به این ندارند که طراحی رابط‌کاربری تحت چه نوع فناوری و زبانی در حل توسعه است و یا ارتباط با پایگاه داده چگونه صورت می‌گیرد چرا که برنامه‌نویسان مرتبط با آن بخش‌ها با استفاده از مهارت های تخصصی خود در آن بخش آن را هندل خواهند کرد. برنامه‌نویسان معمولاً تیم‌های توسعه یک محصول را ایجاد می‌کنند و همگی آن‌ها وابسته یکدیگر هستند بنابراین اگر برنامه‌نویس واحد بک اند شما نتواند به موقع کدهای مورد نیاز بخش فرانت‌اند شما را آماده و تحویل دهد پروژه شما در زمان تعیین شده به نتیجه مطلوبی نخواهد رسید.

3.توسعه دهنده (Developer)

در رابطه با توسعه‌دهنده باید به این توجه داشته باشید که توسعه‌دهنده به تنهایی عنوان نمی‌شود. بنابراین توسعه‌دهنده به صورت‌های مختلفی وجود دارند (توسعه‌دهنده وب، توسعه‌دهندهٔ نرم‌افزار، توسعه‌دهندهٔ موبایل که در رابطه با نوع پلتفرم باز متفاوت هستند؛ توسعه دهنده رابط کاربری، توسعه‌دهندهٔ تجربه کاربری و در نهایت توسعه دهنده فول-استک).

توسعه‌دهنده کسی است که علاوه بر برنامه‌نویس بودن مهارت و دانش کافی در لایه‌های مختلف پروژه در اختیار داشته باشد که متناسب با نوع تخصص نیز متفاوت است. توسعه‌دهنده کسی است که می‌تواند بر اساس نوع پروژه وظایف خاصی را در اختیار بگیرد به عنوان مثال اگر به صورت تیمی بر روی یک پروژه کار می‌کنید که شامل برنامه نویس هایی است که هر کدام بخشی از پروژه را برنامه‌نویسی می‌کنند کافی است یک توسعه‌دهنده داشته باشید تا تمامی کد‌های شما را آنالیز، اشکال زدائی و بررسی کند و در نهایت آن‌ها را با یکدیگر ارتباط داده و تبدیل به یک پروژه قابل استفاده نماید. چرا که توسعه‌دهنده دانش مورد نیاز در لایه‌های مختلف را دارد و می‌داند بخش‌های مختلف یک محصول نرم‌افزاری یا غیره را که چگونه است و چطور باید برنامه‌نویسی شوند به آن‌ها تسلط دارد.

توسعه‌دهنده شخصی است که نباید فقط به یک زبان‌ یا ابزار برنامه‌نویسی اکتفا کند چرا که برای توسعه محصول حتما باید از چند زبان برنامه‌نویسی مورد نیاز در پروژه اطلاعات کاملی داشته و بتواند هر جا که نیاز بود کد‌ای مورد نیاز را توسعه و به نتیجه نهایی تبدیل کند. توجه داشته باشید که یک تیم شامل چندین توسعه دهنده به عنوان یک تیم کاملا حرفه‌ای و زبان زد محسوب می‌شوند برای مثال شرکت‌های بسیار بزرگ نه تنها برنامه‌نویسان حرفه‌ای در تیم خود استخدام می‌کنند بلکه توسعه دهندگانی را از نوع (Full-Stack) در اختیار دارند که مدیریت حساس پروژه را به عهده گرفته و پروژه را با دانشی که دارد به خوبی مدیریت می‌کند و زمانی که جایی پروژه به نکته‌ای برسد که برنامه‌نویسان توانایی حل آن را ندارند توسعه دهنده فول-استک می‌تواند با مهارت‌ها و تجربیات خود آن را حل کند. در رابطه با برنامه‌نویسان فول‌استک و ویژگی‌های این دسته از برنامه‌نویسان را در ادامه آورده شده است.

4.توسعه دهندگان فرانت-اند (UI/UX)

این نوع توسعه‌دهندگان برنامه‌نویسانی هستند که مهارت کاملی در رابطه با لایه‌های مختلف و چندین زبان و فناوری‌های مورد نیاز در بخش User Interface و User Experience را دارند و می‌توانند طراحی مناسبی را متناسب با نوع پروژه و تجریبات مشتری ایجاد و توسعه دهند. این نوع برنامه‌نویسان یکی از مهمترین بخش‌های توسعه‌دهندگان در تیم محسوب می‌شوند که شاید تجربیات و بازخورد‌های مشتری را به سمت این نوع توسعه دهنده ارسال کنند. این نوع توسعه دهندگان علاوه بر داشتن دانش طراحی و تجربه کاربری دانش مرتبط با روانشناسی رنگ‌ها و جلوه‌های بصری دارند که آن‌ها را می‌توانند در قالب برنامه نویسی پیاده سازی کنند.

5.توسعه دهندگان بک-اند

این نوع توسعه‌ دهندگان برنامه‌نویسان بسیار ماهری در بخش لایه‌های زیرین پروژه یعنی (منطقی) دارند که تمامی اطلاعات لازم را در رابطه با لایه‌های زیرین در اختیار دارند و می‌دانند چطور باید با دیتابیس ارتباط برقرار کنند، می‌دانند چطور باید با API‌های سیستم عامل کار کنند و می‌دانند کد‌های خود را بر اساس نوع ABI‌ و API‌ها چگونه باید مدیریت کنند؛ موارد بسیار زیادی که نیاز است در بخش منطقی یک پروژه ایجاد شود را به تنهایی حل و توسعه می‌دهند تا نتایج آن در اختیار توسعه دهنده فرانت اند قرار بگیرد. اما وظیفه‌ای در رابطه با طراحی تجربه‌کاربری و یا رابط کاربری نداشته و نمی‌توانند در این بخش مانور دهند. از این نوع توسعه دهندگان تنها می‌توان انتظار نوشتن کُدهای بی نقص و عالی در لایه‌های پایین را داشت.

6.توسعه دهندگان فول-استک

این نوع توسعه‌‌دهندگان علاوه بر داشتن مهارت‌های مربو به برنامه‌نویسی، توسعه‌دهنده در فرانت‌اند و بک‌اند نیز هستند تجربیات و مهارت‌های بسیار زیادی در شاخه‌های دیگر علوم مهندسی کامپیوتری را دارند که نکته بسیار مهمی است! رسیدن به این درجه از برنامه‌نویسی یعنی یک مهندس کامل کامپیوتر که می‌تواند در تمامی بخش‌های یک پروژه در لایه‌های مختلف نرم‌افزار، سخت‌افزار، شبکه، پلتفرم‌ها و … صاحب نظر باشند و آن را توسعه دهند. یک فول استک به تنهایی می‌تواند رهبری یک پروژه را بر عهده بگیرد و درصورتی که نیاز باشد به تنهایی یک پروژه را از صفر تا صد تولید توسعه و اجرا نماید.

این نوع توسعه‌دهنده‌ها یک مهندس واقعی کامپیوتر (نرم‌افزار) هستند که به خوبی می‌دانند یک محصول نهایی باید تحت چه شرایطی توسعه و اجرا شود. البته به این نکته توجه کنید توسعه‌دهنده‌های فول‌استک هم می‌توانند از لحاض محدودیت دسته‌بندی شوند، بعضی از آن‌ها صرفاً در یک حوزه تسلط کافی دارند، مانند Full Stack Web Developer یا Full Stack Android Developer به معنای این است که این گونه توسعه‌دهنده‌ها می‌توانند یک محصول را در سطح وب به طور کامل طراحی و پیاده سازی کند و یا در حوزهٔ ساخت و ساز اپلیکیشن‌های اندروید یک محصول را به طور کامل طراحی و برنامه‌نویسی کند. در غیر این صورت تنها واژهٔ Full Stack Developer می‌تواند پوشش کاملی بر تسلط کامل یک فرد با تخصص‌های بسیار بالا در ساخت و ساز یک محصول در پلتفرم‌های متنوع  را بدهد که در یک پست جداگانه قبلاً به آن اشاره کرده‌ام:

با توجه به تعاریف مرتبط با عناوین باید سعی کنیم که از اصطلاحات صحیح و عناوین مرتبط با خود استفاده کنیم چرا که در صورتی که شما یک برنامه‌نویس هستید نباید بگویید من یک توسعه‌دهندهٔ وب هستم! این کار باعث ایجاد انتظار از شما خواهد شد که به احتمال بسیار زیاد توانایی انجام آن را نخواهید داشت. حتی برعکس آن ممکن است شما یک توسعه‌دهنده باشید اما بگویید یک کُدر حرفه‌ای هستم! این واقعاً اشتباه است! در این صورت درجه تخصصی خود را به شدت تنزل داده‌اید.

نقل قول

 

در نظر داشته باشید عناوین بسیار دیگری وجود دارد که ممکن است مهارت‌های شما را بهتر پوشش دهند، اما در حوزهٔ برنامه‌نویسی عناوین مطرح شده شناخته‌ترین آن‌ها هستند که در این مقاله آورده شده است.

به عقیدهٔ من مهندسی کامپیوتر با چند عنوان خلاصه نمی‌شود و شما می‌توانید با عنوان مهندسی کامپیوتر مهارت‌های خود را در رزومهٔ خود به طور شفاف مطرح کنید.

 

در فرصت مناسب‌تری در بارهٔ این موضوع، در قالب یک اینفوگرافیک اطلاعاتی نشر خواهم کرد.

×
×
  • جدید...