Amin'sTechLab
2.01K subscribers
572 photos
371 videos
1.29K files
1K links
🔬 Embedded Systems | AI | FPGA | PCB Design
🚀 Exploring tech at the edge of innovation
🧠 Founder of AminTechLab
📍Engineering the future – one bit at a time
Download Telegram
Amin'sTechLab
پشت صحنه‌ی یک دستور در لینوکس!! (۲/۱۰) امروزه دیگر آن دستگاه‌های بزرگ وجود ندارند، اما مفهوم آن‌ها همچنان زنده است. وقتی برنامه‌ای مثل GNOME Terminal، Konsole یا Windows Terminal را باز می‌کنید، در واقع با یک Terminal Emulator کار می‌کنید؛ نرم‌افزاری که رفتار…
پشت صحنه‌ی یک دستور در لینوکس! (۳/۱۰)
ممکن است این سؤال برایتان پیش بیاید که آیا فقط یک شل وجود دارد؟
از آنجایی که شل خودش فقط یک برنامه است، هر توسعه‌دهنده‌ای می‌تواند نسخه‌ی مخصوص خودش را بنویسد. به همین دلیل طی سال‌ها شل‌های مختلفی با امکانات متفاوت به وجود آمده‌اند.
• sh (Bourne Shell)
قدیمی‌ترین شل استاندارد یونیکس که در سال ۱۹۷۷ معرفی شد. اگرچه امکاناتش محدود است، اما هنوز هم پایه‌ی بسیاری از اسکریپت‌های سیستمی محسوب می‌شود.
• Bash (Bourne Again Shell)
محبوب‌ترین شل دنیای لینوکس و انتخاب پیش‌فرض بسیاری از توزیع‌ها. قابلیت‌هایی مثل History، تکمیل خودکار با کلید Tab و امکانات پیشرفته‌ی اسکریپت‌نویسی باعث محبوبیت آن شده است.
• Zsh (Z Shell)
شل مدرنی که این روزها در macOS و بسیاری از سیستم‌های توسعه استفاده می‌شود. اگر نام Oh My Zsh را شنیده باشید، با یکی از معروف‌ترین مجموعه افزونه‌های همین شل آشنا هستید.
• Fish (Friendly Interactive Shell)
اگر به دنبال تجربه‌ای ساده‌تر و کاربرپسندتر باشید، Fish یکی از بهترین انتخاب‌هاست. پیشنهاد خودکار دستورات، رنگ‌بندی مناسب و تنظیمات پیش‌فرض خوب، آن را برای کاربران تازه‌کار و حرفه‌ای جذاب کرده است.
اگر این شل‌ها روی سیستم نصب باشند، کافی است نام آن‌ها را در ترمینال اجرا کنید:
fish

یا
zsh

برای تغییر دائمی شل پیش‌فرض نیز می‌توانید از دستور زیر استفاده کنید:
chsh

در نهایت، همه‌ی این شل‌ها یک هدف مشترک دارند؛ برقراری ارتباط بین شما و سیستم‌عامل، فقط با روش‌ها و امکانات متفاوت.

ادامه دارد...

@Amin_TechLab
4👾2
Amin'sTechLab
پشت صحنه‌ی یک دستور در لینوکس! (۳/۱۰) ممکن است این سؤال برایتان پیش بیاید که آیا فقط یک شل وجود دارد؟ از آنجایی که شل خودش فقط یک برنامه است، هر توسعه‌دهنده‌ای می‌تواند نسخه‌ی مخصوص خودش را بنویسد. به همین دلیل طی سال‌ها شل‌های مختلفی با امکانات متفاوت به…
پشت صحنه‌ی یک دستور در لینوکس! (۴/۱۰)
یکی از زیباترین ایده‌هایی که فلسفه‌ی یونیکس بر پایه‌ی آن شکل گرفته، مفهوم Pipeline است.
اصل ماجرا ساده است:
هر برنامه فقط یک کار را انجام دهد، اما همان یک کار را به بهترین شکل ممکن انجام دهد.

حالا سؤال اینجاست؛ اگر هر برنامه فقط یک وظیفه داشته باشد، چطور کارهای پیچیده انجام می‌شوند؟
پاسخ را شل می‌دهد.
هر برنامه در لینوکس سه کانال استاندارد دارد:
stdin → ورودی برنامه
stdout → خروجی عادی
stderr → خروجی خطا
شل می‌تواند خروجی یک برنامه را مستقیماً به ورودی برنامه‌ی بعدی متصل کند.
مثلاً:
cat /var/log/mylog | grep "error"

در اینجا خروجی دستور cat مستقیماً وارد grep می‌شود و grep فقط خطوطی را نمایش می‌دهد که شامل کلمه‌ی error هستند.
بدون اینکه فایل موقتی ساخته شود یا برنامه‌ی بزرگی نوشته شود.
به همین دلیل ابزارهای یونیکس مثل قطعات لگو هستند؛ هر کدام یک کار انجام می‌دهند و شل آن‌ها را کنار هم قرار می‌دهد تا یک کار بزرگ‌تر ساخته شود.
همین سادگی، یکی از مهم‌ترین دلایل ماندگاری فلسفه‌ی یونیکس است.

ادامه دارد....

@Amin_TechLab
2👾2🔥1
Amin'sTechLab
پشت صحنه‌ی یک دستور در لینوکس! (۴/۱۰) یکی از زیباترین ایده‌هایی که فلسفه‌ی یونیکس بر پایه‌ی آن شکل گرفته، مفهوم Pipeline است. اصل ماجرا ساده است: هر برنامه فقط یک کار را انجام دهد، اما همان یک کار را به بهترین شکل ممکن انجام دهد. حالا سؤال اینجاست؛ اگر هر…
پشت صحنه‌ی یک دستور در لینوکس! (۵/۱۰)
وقتی Enter را فشار می‌دهید، اولین چیزی که به شل می‌رسد چیست؟
از دید ما فقط یک خط متن تایپ شده است؛ اما پشت پرده چندین مرحله اتفاق می‌افتد.
ابتدا ترمینال کلیدهایی که فشار داده‌ایم را دریافت می‌کند.
این داده‌ها از طریق TTY به برنامه‌ای ارسال می‌شوند که در Foreground ترمینال قرار دارد؛ معمولاً همان شل.
در نتیجه شل مستقیماً از صفحه‌کلید چیزی دریافت نمی‌کند، بلکه اطلاعات را از Standard Input خودش می‌خواند.
فرض کنید این دستور را وارد می‌کنیم:
echo myfile

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

ادامه دارد ...
@Amin_TechLab
4👾1
Amin'sTechLab
پشت صحنه‌ی یک دستور در لینوکس! (۵/۱۰) وقتی Enter را فشار می‌دهید، اولین چیزی که به شل می‌رسد چیست؟ از دید ما فقط یک خط متن تایپ شده است؛ اما پشت پرده چندین مرحله اتفاق می‌افتد. ابتدا ترمینال کلیدهایی که فشار داده‌ایم را دریافت می‌کند. این داده‌ها از طریق TTY…
پشت صحنه‌ی یک دستور در لینوکس! (۶/۱۰)
قبل از اجرای هر دستور، شل یک مرحله‌ی بسیار مهم را انجام می‌دهد:
Parsing و Expansion
برای مثال این چهار دستور را ببینید:
echo $HOME
echo "$HOME"
echo '$HOME'
echo *.txt

ظاهر آن‌ها تقریباً مشابه است، اما شل آن‌ها را کاملاً متفاوت تفسیر می‌کند.
شل باید تشخیص دهد:
آیا متغیر محیطی باید جایگزین شود؟
آیا نقل‌قول‌ها مانع Expansion هستند؟
آیا * باید به لیست فایل‌ها تبدیل شود؟
در واقع برنامه‌ی echo هیچ شناختی از $HOME یا *.txt ندارد.
تمام این تبدیل‌ها قبل از اجرای برنامه توسط خود شل انجام می‌شود.
به همین دلیل چیزی که برنامه دریافت می‌کند، الزاماً همان متنی نیست که شما تایپ کرده‌اید.
در حقیقت، شل ابتدا متن خام را به یک دستور قابل فهم تبدیل می‌کند و بعد اجرای آن را آغاز می‌کند.

ادامه دارد...

@Amin_TechLab
2👾2
Amin'sTechLab
پشت صحنه‌ی یک دستور در لینوکس! (۶/۱۰) قبل از اجرای هر دستور، شل یک مرحله‌ی بسیار مهم را انجام می‌دهد: Parsing و Expansion برای مثال این چهار دستور را ببینید: echo $HOME echo "$HOME" echo '$HOME' echo *.txt ظاهر آن‌ها تقریباً مشابه است، اما شل آن‌ها را کاملاً…
پشت صحنه‌ی یک دستور در لینوکس! (۷/۱۰)
بعد از اینکه شل تصمیم گرفت چه برنامه‌ای باید اجرا شود، نوبت به دو مفهوم بسیار مهم در سیستم‌عامل‌های شبه‌یونیکس می‌رسد:
fork و exec
فرض کنید دستور زیر را اجرا می‌کنید:
ls

اولین کاری که شل انجام می‌دهد، ایجاد یک فرآیند جدید است.
این کار با سیستم‌کال fork() انجام می‌شود.
بعد از آن، فرآیند جدید با استفاده از exec() خودش را کاملاً با برنامه‌ی ls جایگزین می‌کند.
در همین زمان، شل اصلی منتظر می‌ماند تا اجرای برنامه تمام شود و سپس دوباره کنترل ترمینال را در اختیار بگیرد.
اما همه‌ی دستورات این مسیر را طی نمی‌کنند.
برخی دستورات اصلاً فایل اجرایی مستقلی ندارند و داخل خود شل پیاده‌سازی شده‌اند؛ به این‌ها Shell Builtins گفته می‌شود.
یکی از بهترین مثال‌ها دستور cd است.
اگر cd یک برنامه‌ی مستقل بود، فقط مسیر کاری همان فرآیند فرزند تغییر می‌کرد و بعد از پایان برنامه، شل همچنان در همان پوشه‌ی قبلی باقی می‌ماند.
به همین دلیل دستوراتی مانند:
cd
pwd
alias
export
توسط خود شل اجرا می‌شوند و نیازی به جستجو در PATH ندارند.

ادامه دارد...

@Amin_TechLab
🔥2👾21
Amin'sTechLab
پشت صحنه‌ی یک دستور در لینوکس! (۷/۱۰) بعد از اینکه شل تصمیم گرفت چه برنامه‌ای باید اجرا شود، نوبت به دو مفهوم بسیار مهم در سیستم‌عامل‌های شبه‌یونیکس می‌رسد: fork و exec فرض کنید دستور زیر را اجرا می‌کنید: ls اولین کاری که شل انجام می‌دهد، ایجاد یک فرآیند جدید…
پشت صحنه‌ی یک دستور در لینوکس! (۸/۱۰)
شل فقط مسئول اجرای برنامه‌ها نیست؛ خودش یک زبان برنامه‌نویسی کامل است.
برای مثال:
name="Ali"
echo "$name"

یا:
if [ -f /etc/passwd ]; then
echo "exists"
fi

و حتی:
for file in *.txt; do
echo "$file"
done

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

ادامه دارد...

@Amin_TechLab
👾31🔥1
Amin'sTechLab
پشت صحنه‌ی یک دستور در لینوکس! (۸/۱۰) شل فقط مسئول اجرای برنامه‌ها نیست؛ خودش یک زبان برنامه‌نویسی کامل است. برای مثال: name="Ali" echo "$name" یا: if [ -f /etc/passwd ]; then echo "exists" fi و حتی: for file in *.txt; do echo "$file" done در این مثال‌ها…
پشت صحنه‌ی یک دستور در لینوکس! (۹/۱۰)
کار شل با اجرای یک برنامه تمام نمی‌شود.
اگر برنامه در Foreground اجرا شده باشد، شل تا پایان اجرای آن منتظر می‌ماند و کنترل ترمینال را دوباره پس می‌گیرد.
به همین دلیل هنگام اجرای برنامه‌های زمان‌بر، تا پایان کار خبری از Prompt جدید نیست.
اما وظایف شل فقط به انتظار کشیدن محدود نمی‌شود.
اگر کاربر کلیدهای Ctrl+C یا Ctrl+Z را فشار دهد، این شل است که باید سیگنال مناسب را مدیریت کند.
همچنین اگر برنامه‌ای را با & در پس‌زمینه اجرا کنیم، باز هم شل مسئول مدیریت آن خواهد بود.
او باید:
وضعیت Jobها را نگه دارد.
پایان اجرای آن‌ها را تشخیص دهد.
در صورت نیاز به کاربر اطلاع دهد.
و هم‌زمان Prompt را برای اجرای دستورات بعدی نمایش دهد.
به همین دلیل شل صرفاً یک اجراکننده‌ی برنامه نیست؛ بلکه مدیر چرخه‌ی اجرای آن‌ها نیز محسوب می‌شود.

ادامه دارد...

@Amin_TechLab
👾31🔥1
Amin'sTechLab
پشت صحنه‌ی یک دستور در لینوکس! (۹/۱۰) کار شل با اجرای یک برنامه تمام نمی‌شود. اگر برنامه در Foreground اجرا شده باشد، شل تا پایان اجرای آن منتظر می‌ماند و کنترل ترمینال را دوباره پس می‌گیرد. به همین دلیل هنگام اجرای برنامه‌های زمان‌بر، تا پایان کار خبری از…
پشت صحنه‌ی یک دستور در لینوکس! (۱۰/۱۰)
اگر تمام این مسیر را مرور کنیم، می‌بینیم پشت یک خط فرمان ساده، دنیای بزرگی پنهان شده است.
ابتدا ترمینال ورودی ما را دریافت می‌کند.
شل آن را می‌خواند، تجزیه و تحلیل می‌کند، متغیرها را گسترش می‌دهد، مسیر برنامه را پیدا می‌کند و تصمیم می‌گیرد چه چیزی باید اجرا شود.
در ادامه، با استفاده از مفاهیمی مانند fork و exec فرآیندهای جدید ایجاد می‌شوند و پس از پایان اجرا، شل دوباره کنترل ترمینال را در اختیار می‌گیرد.
از طرف دیگر، شل خودش نیز یک زبان برنامه‌نویسی کامل است؛ زبانی که امکان تعریف متغیر، شرط، حلقه، توابع و خودکارسازی بسیاری از کارها را فراهم می‌کند.
شاید به همین دلیل است که با وجود ده‌ها محیط گرافیکی مدرن، هنوز هم خط فرمان یکی از قدرتمندترین ابزارهای تعامل با سیستم‌عامل محسوب می‌شود.
شل یک ابزار قدیمی نیست؛ بلکه یکی از بنیادی‌ترین بخش‌های دنیای یونیکس است که بعد از چند دهه، همچنان همان سادگی، انعطاف و قدرت روز اول را حفظ کرده است.

پایان.

@Amin_TechLab
👾31🔥1
خب حالا که ساختار Shell در لینوکس رو باهاش آشنا شدیم .
وقتی خوبیه که Mini-Shell بنویسیم تا دقیقا عملکردش رو درک کنیم .
در ادامه ابتدا این Mini-Shell را به زبان C می‌نویسیم و بعد همان را با زبان Rust بازنویسی میکنیم .

@Amin_TechLab
👾21🔥1
💻 ساخت یک Mini Shell در کمتر از ۵۰ خط کد!
اگر بخواهیم ساده‌ترین نسخه‌ی یک Shell را بنویسیم، فقط به چند قابلیت نیاز داریم:
دریافت یک دستور از کاربر
ساخت یک Process جدید (fork)
اجرای برنامه (execvp)
منتظر ماندن تا پایان اجرای برنامه (waitpid)
کد زیر دقیقاً همین کار را انجام می‌دهد.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>

int main(void)
{
char line[256];

while (1)
{
printf("mini-shell> ");

if (!fgets(line, sizeof(line), stdin))
break;

line[strcspn(line, "\n")] = '\0';

if (strcmp(line, "exit") == 0)
break;

char *argv[32];
int argc = 0;

char *token = strtok(line, " ");

while (token && argc < 31)
{
argv[argc++] = token;
token = strtok(NULL, " ");
}

argv[argc] = NULL;

pid_t pid = fork();

if (pid == 0)
{
execvp(argv[0], argv);
perror("execvp");
exit(EXIT_FAILURE);
}
else
{
waitpid(pid, NULL, 0);
}
}

return 0;
}

این کد چگونه کار می‌کند؟
🔹 fgets()
یک خط از کاربر دریافت می‌کند.
🔹 strtok()
رشته را بر اساس فاصله به آرگومان‌های مختلف تقسیم می‌کند.
برای مثال:
ls -l /home

به این تبدیل می‌شود:
argv[0] = "ls"
argv[1] = "-l"
argv[2] = "/home"

🔹 fork()
از فرآیند فعلی یک نسخه‌ی جدید ایجاد می‌کند.
بعد از این تابع دو Process وجود دارد:
Parent (شل)
Child (برنامه‌ای که قرار است اجرا شود)
🔹 execvp()
فرآیند فرزند را با برنامه‌ی موردنظر جایگزین می‌کند.
اگر بنویسید:
ls -l

در واقع Child دیگر Mini Shell نیست؛ تبدیل به برنامه‌ی ls می‌شود.
🔹 waitpid()
شل اصلی منتظر می‌ماند تا اجرای برنامه تمام شود و سپس دوباره Prompt را نمایش می‌دهد.
این دقیقاً همان چرخه‌ای است که تقریباً تمام Shellهای لینوکس (مانند Bash و Zsh) برای اجرای برنامه‌های خارجی انجام می‌دهند:
Read Command


Parse Arguments


fork()


execvp()


Program Runs


waitpid()


Show Prompt Again

این نمونه فقط یک Shell آموزشی است و قابلیت‌هایی مثل Pipeline (|)، Redirect (>)، متغیرهای محیطی، Wildcard (*)، Job Control، Builtins و Quote Parsing را پیاده‌سازی نمی‌کند؛ اما برای درک نحوه‌ی اجرای دستورات در لینوکس، یکی از بهترین نقطه‌های شروع محسوب می‌شود.

@Amin_TechLab
2👾2🔥1
💻حالا همان Mini Shell که در پست قبل با زبان C نوشتیم را با Rust بازنویسی می‌کنیم.
جالب است بدانید که در Rust دیگر خبری از fork() و execvp() نیست. کتابخانه‌ی استاندارد Rust این جزئیات سطح پایین را پشت یک API ساده مخفی کرده و ما فقط با std::process::Command کار می‌کنیم.
کد زیر دقیقاً همان Shell ساده‌ی پست قبل را پیاده‌سازی می‌کند:
use std::io::{self, Write};
use std::process::Command;

fn main() {
loop {
print!("mini-shell> ");
io::stdout().flush().unwrap();

let mut line = String::new();
io::stdin().read_line(&mut line).unwrap();

let line = line.trim();

if line.is_empty() {
continue;
}

if line == "exit" {
break;
}

let args: Vec<&str> = line.split_whitespace().collect();

let status = Command::new(args[0])
.args(&args[1..])
.status();

match status {
Ok(status) => {
println!("Exit Status: {}", status);
}
Err(e) => {
eprintln!("Error: {}", e);
}
}
}
}

این برنامه چگونه کار می‌کند؟
🔹 دریافت ورودی
ابتدا یک خط از کاربر خوانده می‌شود:
let mut line = String::new();
io::stdin().read_line(&mut line).unwrap();

🔹 تجزیه دستور
مانند نسخه‌ی C، ورودی بر اساس فاصله جدا می‌شود:
let args: Vec<&str> =
line.split_whitespace().collect();

اگر کاربر بنویسد:
ls -l /home

بردار args به این شکل خواهد بود:
["ls", "-l", "/home"]

🔹 اجرای برنامه
در این قسمت تفاوت اصلی Rust و C دیده می‌شود:
Command::new(args[0])
.args(&args[1..])
.status();

کافی است نام برنامه و آرگومان‌ها را مشخص کنیم.
کتابخانه‌ی استاندارد Rust در سیستم‌های شبه‌یونیکس خودش در پشت صحنه عملیات fork → exec → wait را انجام می‌دهد و نتیجه‌ی اجرای برنامه را به ما برمی‌گرداند.
به عبارت دیگر، ما همان رفتار Shell را داریم، اما بدون اینکه مستقیماً با fork() یا execvp() کار کنیم.
چرا این کد این‌قدر کوتاه‌تر است؟
در نسخه‌ی C مجبور بودیم:
حافظه را مدیریت کنیم.
رشته را Tokenize کنیم.
fork() را صدا بزنیم.
execvp() را اجرا کنیم.
خطاها را مدیریت کنیم.
با waitpid() منتظر پایان فرآیند بمانیم.
اما در Rust، بیشتر این پیچیدگی‌ها داخل std::process::Command پنهان شده‌اند و API ساده‌تری در اختیار برنامه‌نویس قرار می‌گیرد.
آیا Rust واقعاً از fork استفاده نمی‌کند؟
یکی از سؤالات رایج همین است.
خیر؛ این فقط ظاهر ماجراست.
در لینوکس، زمانی که از Command::status() یا Command::spawn() استفاده می‌کنید، Rust در نهایت از مکانیزم‌های سیستم‌عامل برای ایجاد فرآیند جدید استفاده می‌کند. بسته به سیستم‌عامل و نسخه‌ی آن، این کار ممکن است با fork()، vfork()، posix_spawn() یا مکانیزم‌های مشابه انجام شود.
بنابراین Rust چیزی را جایگزین سیستم‌عامل نکرده است؛ فقط یک Abstraction سطح بالا روی همان APIهای قدیمی یونیکس ساخته تا توسعه‌دهنده با کد کمتر، خواناتر و ایمن‌تر همان نتیجه را به دست آورد.
به همین دلیل اگر مفاهیم fork، exec و wait را در C یاد بگیرید، درک رفتار Command در Rust نیز بسیار ساده‌تر خواهد بود.

@Amin_TechLab
🔥2👾21
💻 یکی از خطرناک‌ترین مفاهیم در زبان C: Sequence Point و Unsequenced Modifications (بخش اول)
اگر سال‌ها با زبان C کار کرده باشید، احتمالاً حداقل یک بار با کدی مواجه شده‌اید که روی یک کامپایلر درست کار می‌کند، اما روی کامپایلر یا سطح Optimization دیگر، نتیجه متفاوتی می‌دهد.
در بسیاری از موارد، دلیل این رفتار یک مفهوم مهم در استاندارد C است:
Unsequenced Modifications

این مفهوم یکی از رایج‌ترین دلایل Undefined Behavior در زبان C محسوب می‌شود.
فرض کنید کد زیر را بنویسید:
#include <stdio.h>

int main()
{
int i = 5;

printf("%d\n", i++ + ++i);

return 0;
}

به نظر شما خروجی چیست؟
12 ؟
13 ؟
14 ؟

پاسخ صحیح این است:
هیچ پاسخ درستی وجود ندارد.
طبق استاندارد C، این کد Undefined Behavior است.
چرا؟
بیایید عبارت زیر را بررسی کنیم:
i++ + ++i

در این عبارت، متغیر i دو بار تغییر می‌کند:
i++

و
++i

اما استاندارد C مشخص نمی‌کند که کدام‌یک باید ابتدا اجرا شود.
کامپایلر می‌تواند:
ابتدا i++ را اجرا کند.
ابتدا ++i را اجرا کند.
حتی بخشی از هر دو را هم‌زمان بهینه‌سازی کند.
هیچ ترتیب مشخصی وجود ندارد.
Sequence Point چیست؟
در زبان C، بعضی نقاط وجود دارند که استاندارد تضمین می‌کند تمام عملیات قبلی کامل شده‌اند و سپس اجرای بخش بعدی آغاز می‌شود.
به این نقاط می‌گویند:
Sequence Point
در استانداردهای جدید C بیشتر از اصطلاح‌های Sequenced Before و Unsequenced استفاده می‌شود، اما هنوز هم بسیاری از منابع آموزشی از عبارت Sequence Point استفاده می‌کنند.
چه زمانی مشکل ایجاد می‌شود؟
اگر در یک عبارت:
یک متغیر را بیش از یک بار تغییر دهید.
یا آن را هم‌زمان بخوانید و تغییر دهید.
بدون اینکه ترتیب اجرای این عملیات مشخص باشد.
برنامه وارد Undefined Behavior می‌شود.
این یعنی استاندارد C دیگر هیچ تضمینی درباره نتیجه اجرای برنامه نمی‌دهد.
در بخش دوم، چند مثال واقعی دیگر را بررسی می‌کنیم و خواهیم دید چرا این دسته از باگ‌ها فقط با تغییر کامپایلر یا فعال کردن Optimization ظاهر می‌شوند.

@Amin_TechLab
4🔥2👾1
Amin'sTechLab
💻 یکی از خطرناک‌ترین مفاهیم در زبان C: Sequence Point و Unsequenced Modifications (بخش اول) اگر سال‌ها با زبان C کار کرده باشید، احتمالاً حداقل یک بار با کدی مواجه شده‌اید که روی یک کامپایلر درست کار می‌کند، اما روی کامپایلر یا سطح Optimization دیگر، نتیجه…
💻 یکی از خطرناک‌ترین مفاهیم در زبان C: Sequence Point و Unsequenced Modifications (بخش دوم)
در بخش قبل دیدیم که اگر یک متغیر در یک عبارت چند بار تغییر کند و ترتیب اجرای این تغییرات مشخص نباشد، برنامه وارد Undefined Behavior می‌شود.
حالا چند مثال دیگر را بررسی کنیم.
مثال اول:
int i = 0;

i = i++;

بسیاری تصور می‌کنند مقدار i برابر ۱ خواهد شد.
اما این کد نیز Undefined Behavior است.
مثال دوم:
int i = 3;

printf("%d %d\n", i++, i++);

به نظر شما خروجی چیست؟
3 4

یا
4 3

یا حتی
4 4

واقعیت این است که استاندارد C هیچ‌کدام از این خروجی‌ها را تضمین نمی‌کند.
چون ترتیب ارزیابی آرگومان‌های یک تابع در زبان C مشخص نشده است.
مثال سوم:
int a = 10;

a = a++ + 5;

در این مثال نیز مقدار a هم‌زمان خوانده و تغییر داده می‌شود، بدون اینکه ترتیب اجرای این عملیات مشخص باشد.
نتیجه؟
باز هم Undefined Behavior.
چرا کامپایلر این کدها را خطا نمی‌گیرد؟
زیرا استاندارد C فقط مشخص می‌کند که رفتار این برنامه تعریف‌نشده است؛ اما کامپایلر را مجبور نمی‌کند که حتماً برای آن خطا یا حتی Warning صادر کند.
کامپایلر فرض می‌کند شما قوانین زبان را رعایت کرده‌اید و بر همان اساس کد را بهینه می‌کند.
به همین دلیل ممکن است:
gcc main.c

یک خروجی مشخص تولید کند.
اما همان برنامه با:
gcc main.c -O2

یا حتی روی کامپایلر دیگری، رفتار کاملاً متفاوتی داشته باشد.
چگونه از این مشکل جلوگیری کنیم؟
به جای نوشتن عبارت‌های پیچیده مانند:
printf("%d\n", i++ + ++i);

کد را به چند مرحله ساده تقسیم کنید:
int a = i;
i++;

int b = i;
i++;

printf("%d\n", a + b);

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

شاید این نوع کدها امروز روی سیستم شما درست اجرا شوند، اما هیچ تضمینی وجود ندارد که فردا، با یک نسخه جدید از کامپایلر یا فقط با فعال کردن Optimization، همان رفتار را حفظ کنند.
به همین دلیل برنامه‌نویسان حرفه‌ای C و Embedded از نوشتن عبارت‌های پیچیده‌ای که هم‌زمان یک متغیر را چند بار تغییر می‌دهند، خودداری می‌کنند.
در زبان C، ساده نوشتن فقط خوانایی را افزایش نمی‌دهد؛ بلکه از ورود برنامه به یکی از خطرناک‌ترین انواع Undefined Behavior نیز جلوگیری می‌کند. ⚠️

@Amin_TechLab
👍32🔥2👾2
🚀 بخش اول | آیا وقت بازنویسی PostgreSQL رسیده است؟
سال‌هاست PostgreSQL به عنوان یکی از قدرتمندترین دیتابیس‌های متن‌باز شناخته می‌شود؛ اما معماری آن متعلق به دهه‌ها قبل است.
تیم Turso حالا پروژه‌ای جاه‌طلبانه را آغاز کرده است:
ساخت یک نسخه مدرن از PostgreSQL با زبان Rust، اما نه با یک Rewrite ساده!
ایده اصلی این است که هسته Rust-based تورسو به یک "LLVM برای دیتابیس‌ها" تبدیل شود؛ یعنی یک Core مشترک که موتورهای مختلف دیتابیس بتوانند روی آن پیاده‌سازی شوند.
در این معماری:
PostgreSQL فقط یک Frontend خواهد بود.
SQLite اولین Frontend این هسته است.
در آینده حتی موتورهای دیگری نیز می‌توانند روی همین زیرساخت ساخته شوند.
هدف فقط سازگاری با PostgreSQL نیست؛ بلکه ارائه معماری‌ای مدرن‌تر با قابلیت‌هایی مانند:
اجرای Async واقعی
بهره‌گیری از ایمنی حافظه Rust
اجرای Embedded و Browser Native
حذف محدودیت‌های معماری قدیمی PostgreSQL
امکان توسعه ساده‌تر قابلیت‌های جدید
به بیان دیگر، این پروژه می‌خواهد PostgreSQL را برای نسل بعدی نرم‌افزارها بازتعریف کند، نه اینکه صرفاً آن را به Rust ترجمه کند.

@Amin_Techlab
👾2🔥1
Amin'sTechLab
🚀 بخش اول | آیا وقت بازنویسی PostgreSQL رسیده است؟ سال‌هاست PostgreSQL به عنوان یکی از قدرتمندترین دیتابیس‌های متن‌باز شناخته می‌شود؛ اما معماری آن متعلق به دهه‌ها قبل است. تیم Turso حالا پروژه‌ای جاه‌طلبانه را آغاز کرده است: ساخت یک نسخه مدرن از PostgreSQL…
🔬 بخش دوم | معماری فنی؛ PostgreSQL روی یک Runtime جدید
اگر تصور می‌کنید Turso در حال Rewrite کردن PostgreSQL با Rust است، در واقع داستان بسیار جذاب‌تر از این است.
ایده اصلی، جداسازی رابط PostgreSQL از موتور اجرای دیتابیس است.
در معماری فعلی PostgreSQL تقریباً تمام اجزای سیستم—از Parser و Planner گرفته تا Executor، Storage Engine، Buffer Manager و MVCC—به‌شدت به یکدیگر وابسته هستند. همین وابستگی باعث شده توسعه قابلیت‌های جدید، اجرای Async، استفاده در Edge و WebAssembly یا حتی تغییرات اساسی در Storage بسیار دشوار باشد.
تیم Turso این وابستگی را می‌شکند.
معماری پیشنهادی به این صورت است:
PostgreSQL Wire Protocol

SQL Parser

PostgreSQL AST

Query Planner/Translator

Turso IR / Bytecode

Limbo Runtime Engine

Storage + MVCC + Replication

به جای اجرای مستقیم Queryها در Executor سنتی PostgreSQL، دستورات SQL پس از Parse شدن به یک Intermediate Representation (IR) یا Bytecode تبدیل می‌شوند و سپس توسط Runtime موتور Limbo اجرا خواهند شد.
این دقیقاً همان ایده‌ای است که LLVM در دنیای کامپایلرها پیاده‌سازی کرد؛ یعنی یک لایه میانی که چندین Frontend و Backend می‌توانند از آن استفاده کنند.
مزیت‌های این معماری عبارت‌اند از:
Memory Safety
تمام هسته سیستم با Rust توسعه داده می‌شود؛ بنابراین بخش بزرگی از باگ‌های رایج C مانند Buffer Overflow، Use-after-Free و Data Race حذف می‌شوند.
Storage Engine مستقل
Storage دیگر محدود به معماری قدیمی PostgreSQL نیست و می‌تواند برای SSD، Edge Computing و Object Storage بهینه شود.
Async Runtime واقعی
برخلاف PostgreSQL که مدل Process-per-Connection دارد، Runtime جدید می‌تواند بر پایه Async I/O و Event Loop اجرا شود؛ موضوعی که مصرف حافظه و تعداد Connectionهای همزمان را بهبود می‌دهد.
ماژولار شدن اجزای دیتابیس
Planner، Executor، Storage Engine و Replication به صورت مؤلفه‌های مستقل طراحی می‌شوند؛ بنابراین اضافه کردن قابلیت‌هایی مانند Vector Search، Columnar Storage یا موتورهای Query جدید بسیار ساده‌تر خواهد بود.
Embedded و WebAssembly
از آنجا که Limbo یک Runtime سبک مبتنی بر Rust است، امکان اجرای دیتابیس داخل Browser (WASM)، Edge Functionها و حتی اپلیکیشن‌های Local-first فراهم می‌شود؛ قابلیتی که با PostgreSQL کلاسیک تقریباً غیرممکن است.
نکته مهم اینجاست که هدف Turso جایگزین کردن اکوسیستم PostgreSQL نیست؛ بلکه حفظ سازگاری کامل با پروتکل شبکه، Dialect زبان SQL و ابزارهای موجود (مانند pgAdmin، libpq، ORMها و Driverهای PostgreSQL) است، در حالی که موتور اجرا کاملاً مدرن و بازطراحی شده خواهد بود.
اگر این معماری به بلوغ برسد، احتمالاً برای اولین بار شاهد جدایی واقعی بین PostgreSQL به‌عنوان Interface و PostgreSQL به‌عنوان Database Engine خواهیم بود؛ تغییری که می‌تواند آینده دیتابیس‌های رابطه‌ای را مانند نقش LLVM در دنیای کامپایلرها متحول کند.

@Amin_Techlab
👾2🔥1
Harmonic Firmware Initiative (HFI) aims to create, standardize, and maintain — for RISC-V systems — something the x86 world has taken for granted for forty years: a genuine power-on firmware experience. You switch the machine on and it presents itself on its own display — a firmware identity and POST screen, hardware enumeration, a Set-Up configuration editor, and boot selection — the way every PC has since the 1980s, and the way no bare RISC-V board does today.

@Amin_Techlab
👍1🔥1👾1
HFI-WhitePaper.pdf
215.8 KB
Harmonic Firmware Initiative (HFI) aims to create, standardize, and maintain — for RISC-V systems — something the x86 world has taken for granted for forty years: a genuine power-on firmware experience

@Amin_Techlab
👍1🔥1👾1
🚀 گام مهم Rust for Linux: اولین Abstraction برای Power Supply و اولین Driver شارژر باتری با Rust
پروژه Rust for Linux یک RFC جدید منتشر کرده که یکی از مهم‌ترین قدم‌ها برای گسترش Rust در زیرسیستم‌های کرنل محسوب می‌شود.
در این RFC، سه تغییر مهم پیشنهاد شده است:
اضافه شدن APIهای SMBus برای I2C در Rust
طراحی اولین Abstraction برای Power Supply Class
پیاده‌سازی اولین Driver شارژر باتری (SMB347) با زبان Rust
چرا این RFC مهم است؟
تا امروز، اگر قصد داشتید یک Driver مربوط به باتری یا شارژر را با Rust بنویسید، دو مانع اساسی وجود داشت:
• هیچ Abstraction امنی برای Power Supply Class در Rust وجود نداشت.
• I2C Client فقط عملیات مدیریت Device را انجام می‌داد و امکان خواندن یا نوشتن Registerهای سخت‌افزار را فراهم نمی‌کرد.
در نتیجه، توسعه Driverهای واقعی تقریباً غیرممکن بود.
بخش اول: توسعه APIهای I2C
در این RFC توابعی مانند:
smbus_read_byte_data()
smbus_write_byte_data()
smbus_update_bits()
به I2C Client اضافه شده‌اند.
تابع smbus_update_bits() همان الگوی معروف Read → Modify → Write را پیاده‌سازی می‌کند؛ الگویی که تقریباً در تمام Driverهای کرنل برای تغییر بخشی از بیت‌های یک Register استفاده می‌شود.
بخش دوم: Power Supply Abstraction
مهم‌ترین قسمت این RFC، معرفی یک Abstraction جدید برای زیرسیستم Power Supply است.
در این طراحی، Driver فقط کافی است:
• نام Device را مشخص کند.
• نوع Power Supply را تعیین کند.
• لیست Propertyهای قابل پشتیبانی را معرفی کند.
• تابع get_property() را پیاده‌سازی کند.
تمام جزئیات ارتباط با Power Supply Core در پشت یک API ایمن پنهان شده است.
علاوه بر این، Registration نیز با الگوی RAII طراحی شده تا فرآیند Register و Unregister شدن Driver به‌صورت خودکار مدیریت شود.
بخش سوم: اولین Driver واقعی
برای اثبات کارایی این Abstraction، توسعه‌دهنده یک نسخه Rust از Driver مربوط به شارژر Summit SMB347 پیاده‌سازی کرده است.
این Driver:
• از طریق I2C با چیپ ارتباط برقرار می‌کند.
• Registerهای وضعیت را می‌خواند.
• وضعیت شارژ را به Power Supply Core گزارش می‌دهد.
• اطلاعاتی مانند:
STATUS
ONLINE
CHARGE_TYPE
را از طریق مسیر استاندارد:
/sys/class/power_supply
در اختیار فضای کاربر قرار می‌دهد.
از دید کاربران لینوکس، این Driver دقیقاً مانند Driverهای نوشته‌شده با C رفتار می‌کند.
نکته جالب
در فرآیند Review، یکی از توسعه‌دهندگان Rust for Linux پیشنهاد کرد که APIهای SMBus به‌صورت توابع مستقل اضافه نشوند و در عوض، I2cClient مستقیماً Trait استاندارد kernel::io::Io را پیاده‌سازی کند.
نویسنده RFC نیز این پیشنهاد را پذیرفت و اعلام کرد که نسخه بعدی Patch بر اساس معماری جدید بازنویسی خواهد شد.
این دقیقاً همان چیزی است که توسعه کرنل لینوکس را جذاب می‌کند؛ طراحی APIها قبل از Merge شدن، چندین بار توسط Maintainerها بازبینی و اصلاح می‌شوند تا در نهایت بهترین معماری ممکن وارد Mainline شود.
اگر این RFC در نهایت پذیرفته شود، یکی از زیرسیستم‌های مهم کرنل یعنی Power Supply نیز به فهرست بخش‌هایی اضافه خواهد شد که توسعه Driverهای آن با Rust امکان‌پذیر است؛ گامی دیگر در مسیر گسترش تدریجی Rust در هسته لینوکس.

@Amin_Techlab
👍21
🔍 نگاهی به تغییرات کد برای Power Supply و اولین Driver شارژر باتری با Rust :
این RFC تنها یک ایده تئوری نیست؛ در مجموع بیش از ۳۷۰ خط کد جدید به هسته لینوکس اضافه می‌کند که شامل دو ماژول Rust جدید و یک Driver کامل است.
در Patch اول، کلاس I2cClient قابلیت‌های جدیدی برای کار با رجیسترهای سخت‌افزار دریافت می‌کند. سه متد جدید شامل:
smbus_read_byte_data()
smbus_write_byte_data()
smbus_update_bits()
اضافه شده‌اند.
دو تابع اول، Wrapperهای ایمن (Safe Wrapper) روی توابع C مربوط به SMBus هستند و تمامی عملیات Unsafe و FFI را در داخل خود مخفی می‌کنند. به این ترتیب، توسعه‌دهنده Driver بدون نیاز به کار با Pointerها یا فراخوانی مستقیم APIهای C می‌تواند رجیسترهای سخت‌افزار را بخواند یا تغییر دهد.
تابع smbus_update_bits() نیز یکی از پرکاربردترین الگوهای توسعه Driver را پیاده‌سازی می‌کند؛ یعنی عملیات Read → Modify → Write. این تابع ابتدا مقدار رجیستر را می‌خواند، تنها بیت‌های موردنظر را تغییر می‌دهد و در صورتی که مقدار واقعاً تغییر کرده باشد، دوباره آن را روی سخت‌افزار می‌نویسد. این همان الگویی است که در بسیاری از Driverهای C کرنل نیز استفاده می‌شود.
در Patch دوم، فایل جدید rust/kernel/power_supply.rs اضافه شده که در واقع یک لایه Abstraction برای زیرسیستم Power Supply است.
در این فایل یک Trait جدید با نام Driver تعریف شده که هر Driver تنها کافی است چهار بخش اصلی را پیاده‌سازی کند:
نام Device
نوع Power Supply
لیست Propertyهای قابل پشتیبانی
تابع get_property()
سپس یک Callback عمومی (get_property_trampoline) ارتباط بین Power Supply Core که در C نوشته شده و Driver نوشته‌شده با Rust را برقرار می‌کند. به این ترتیب تمام جزئیات FFI در یک نقطه متمرکز شده و بقیه Driver کاملاً Safe Rust باقی می‌ماند.
یکی دیگر از نکات جالب این Patch، استفاده از الگوی RAII است. ساختار Registration مسئول ثبت Driver در Power Supply Core است و هنگام Drop شدن، به صورت خودکار power_supply_unregister() را فراخوانی می‌کند. بنابراین مدیریت چرخه عمر Driver نیز مطابق الگوهای Rust انجام می‌شود.
در Patch سوم نیز یک Driver واقعی برای شارژر SMB347 پیاده‌سازی شده است.
این Driver مجموعه‌ای از ثابت‌های مربوط به رجیسترهای سخت‌افزار، بیت‌ها و وضعیت‌های مختلف چیپ را تعریف می‌کند و سپس با استفاده از Abstractionهای جدید، وضعیت شارژ، آنلاین بودن منبع تغذیه و نوع شارژ را از روی رجیسترهای سخت‌افزار استخراج کرده و از طریق Power Supply Framework در اختیار فضای کاربر قرار می‌دهد.
در واقع این Driver اولین مصرف‌کننده (First Consumer) از Abstraction جدید Power Supply است و نشان می‌دهد که API طراحی‌شده واقعاً برای توسعه Driverهای واقعی قابل استفاده است، نه صرفاً یک نمونه آزمایشی.

@Amin_Techlab
👍3
🦀 برسی کدهای اضافه شده Rust :
یکی از قسمت‌های جالب این RFC، نحوه طراحی APIها در Rust است. تقریباً تمام بخش‌های Unsafe فقط در مرز ارتباط با کدهای C قرار گرفته‌اند و منطق Driver کاملاً با Safe Rust نوشته شده است.
۱- Wrapper ایمن روی APIهای C
به جای اینکه Driver مستقیماً تابع C زیر را فراخوانی کند:
i2c_smbus_read_byte_data(...)

یک Wrapper امن در I2cClient اضافه شده است:
pub fn smbus_read_byte_data(&self, command: u8) -> Result<u8> {
let ret = unsafe {
bindings::i2c_smbus_read_byte_data(self.as_raw(), command)
};

if ret < 0 {
Err(Error::from_errno(ret))
} else {
Ok(ret as u8)
}
}

در اینجا تنها بخش Unsafe همان فراخوانی تابع C است. بعد از آن، مقدار بازگشتی به یک Result<u8> استاندارد Rust تبدیل می‌شود و Driver دیگر نیازی به بررسی کدهای خطای C ندارد.
۲- پیاده‌سازی الگوی معروف Read → Modify → Write
یکی از متدهای بسیار کاربردی که اضافه شده:
pub fn smbus_update_bits(...)

درون آن ابتدا مقدار رجیستر خوانده می‌شود:
let old = self.smbus_read_byte_data(command)?;

سپس فقط بیت‌های موردنظر تغییر می‌کنند:
let new = (old & !mask) | (value & mask);

و تنها در صورتی که مقدار واقعاً تغییر کرده باشد:
if new != old {
self.smbus_write_byte_data(command, new)?;
}

نوشتن روی سخت‌افزار انجام می‌شود.
این دقیقاً همان الگویی است که تقریباً در اکثر Driverهای C لینوکس نیز دیده می‌شود.
۳- طراحی Driver با Trait
به جای استفاده از Structهای متعدد و Callbackهای C، تنها کافی است Driver این Trait را پیاده‌سازی کند:
pub trait Driver {
const NAME: &'static CStr;
const TYPE: Type;
const PROPERTIES: &'static [Property];

fn get_property(...)
}

این طراحی باعث می‌شود هر Driver فقط روی منطق خود تمرکز کند و تمام جزئیات مربوط به Power Supply Core در پشت Abstraction پنهان بماند.
۴- استفاده از RAII برای مدیریت Driver
در Rust یک Struct به نام Registration معرفی شده است.
نکته جالب این است که هنگام از بین رفتن این شیء:
impl Drop for Registration {
fn drop(&mut self) {
unsafe {
bindings::power_supply_unregister(self.psy);
}
}
}

تابع power_supply_unregister() به صورت خودکار اجرا می‌شود.
در نتیجه توسعه‌دهنده دیگر نگران آزادسازی منابع یا فراموش کردن Unregister کردن Driver نخواهد بود؛ این کار به کمک مکانیزم Drop در Rust انجام می‌شود.
۵- تعریف Driver واقعی فقط در چند خط
در Driver مربوط به SMB347 تنها با چند ثابت می‌توان مشخص کرد که Driver چه قابلیت‌هایی دارد:
const NAME: &'static CStr = c"smb347-mains";

const PROPERTIES: &'static [Property] = &[
PROP_STATUS,
PROP_ONLINE,
PROP_CHARGE_TYPE,
];

و سپس تنها تابعی که باید پیاده‌سازی شود:
fn get_property(...)

که وضعیت شارژر را از رجیسترهای سخت‌افزار می‌خواند و آن را به Power Supply Framework گزارش می‌دهد.
این طراحی باعث شده حجم زیادی از کدهای تکراری Driverهای C حذف شوند و توسعه Driver بیشتر شبیه پیاده‌سازی یک Trait در Rust باشد تا کار با Callbackها و Pointerهای متعدد.
۶- نکته‌ای که توجه Maintainerها را جلب کرد
یکی از جالب‌ترین قسمت‌های Review این RFC مربوط به همین چند خط بود:
pub fn register<T: Driver + 'static>(dev: &Device)

Alice Ryhl پیشنهاد داد که به جای &Device از &Device<Bound> استفاده شود تا از نظر Type System تضمین شود که Driver فقط روی Deviceهایی که کاملاً Bind شده‌اند ثبت می‌شود.
همچنین پیشنهاد کرد این تابع به جای یک تابع آزاد، به شکل:
Registration::new(...)

بازطراحی شود تا با سایر APIهای Rust Kernel هماهنگ باشد.
این بازخوردها نشان می‌دهد که در پروژه Rust for Linux، علاوه بر عملکرد، طراحی API و سازگاری با الگوهای Rust نیز اهمیت بسیار زیادی دارد.

@Amin_Techlab
👍3
🚀 AMTerminal v1.0.0 منتشر شد!
بعد از مدت‌ها توسعه، اولین نسخه عمومی AMTerminal  آماده است.

AMTerminal  یک شل (Shell) و محیط خط فرمان (CLI) مدرن است که با زبان Rust  توسعه داده شده و روی Windows، Linux و macOS اجرا می‌شود.

برخی از قابلیت‌ها:
• پشتیبانی از Pipe (|)
• پشتیبانی از Redirection (>, >>, <)
• همراهPrompt هوشمند با نمایش وضعیت Git
• تکمیل خودکار (Tab Completion)
• ذخیره و جستجوی History
• رنگ‌بندی و Prompt کاملاً قابل شخصی‌سازی
• اجرای دستورات داخلی و خارجی
• عملکرد سریع و سبک
📥 از شما دعوت می‌کنم نسخه اول را دانلود و امتحان کنید.
https://github.com/Amin98Hosseini/AMTerminal_Rust/releases/tag/AMT-v1.0.0-rc.1

💬 اگر باگ، پیشنهاد یا ایده‌ای برای بهتر شدن AMTerminal دارید، حتماً در GitHub ثبت کنید یا برای من ارسال کنید. بازخورد شما نقش مهمی در توسعه
نسخه‌های بعدی خواهد داشت.

⭐️ اگر پروژه را دوست داشتید، فراموش نکنید به آن در GitHub یک Star بدهید.
 
لینک گیت هاب پروژه :
https://github.com/Amin98Hosseini/AMTerminal_Rust

@Amin_Techlab
👾32👍1🔥1