1 tháng học Rust - ứng dụng CLI đầu tiên

1 tháng học Rust - ứng dụng CLI đầu tiên

Tin ngắn hàng ngày dành cho bạn
  • Hơn 1 tuần nay mình không đăng bài, không phải không có gì để viết mà đang tìm cách để phân phối nội dung có giá trị hơn trong thời đại AI đang bùng nổ mạnh mẽ như thế này.

    Như từ hồi đầu năm đã chia sẻ, số lượng người truy cập vào trang blog của mình đang dần ít đi. Khi xem thống kê, lượng người dùng trong 6 tháng đầu năm 2025 đã giảm 30% so với cùng kì năm ngoái, 15% so với 6 tháng cuối năm 2024. Như vậy một sự thật là người dùng đang rời bỏ dần đi. Nguyên nhân do đâu?

    Mình nghĩ lý do lớn nhất là thói quen của người dùng đã thay đổi. Họ tìm thấy blog chủ yếu qua các công cụ tìm kiếm, trong đó lớn nhất là Google. Gần 1/2 số lượng người dùng quay trở lại blog mà không cần thông qua bước tìm kiếm. Đó là một tín hiệu đáng mừng nhưng vẫn không đủ để tăng lượng người dùng mới. Chưa kể giờ đây, Google đã ra mắt tính năng AI Search Labs - tức là AI hiển thị luôn nội dung tổng hợp khi người dùng tìm kiếm, điều đó càng khiến cho khả năng người dùng truy cập vào trang web thấp hơn. Một điều thú vị là khi Search Labs được giới thiệu, thì các bài viết bằng tiếng Anh đã soán ngôi trong bảng xếp hạng truy cập nhiều nhất.

    Một bài viết của mình thường rất dài, có khi lên đến cả 2000 chữ. Mà để viết ra được một bài như thế tốn nhiều thời gian. Nhiều bài viết ra chẳng có ai đọc là điều bình thường. Mình biết và chấp nhận vì không phải ai cũng gặp phải vấn đề đang nói đến. Viết đối với mình như một cách để rèn luyện sự kiên nhẫn và cả tư duy. Viết ra mà giúp được cả ai đó là một điều tuyệt vời.

    Vậy nên mình đang nghĩ sẽ tập trung vào nội dung ngắn và trung bình để viết được nhiều hơn. Nội dung dài chỉ khi muốn viết chi tiết hoặc đi sâu về một chủ đề nào đó. Nên là đang tìm cách thiết kế lại trang blog. Mọi người cùng chờ nha 😄

    » Xem thêm
  • CloudFlare đã giới thiệu tính năng pay per crawl để tính phí cho mỗi lần AI "cào" dữ liệu trên trang web của bạn. Là sao ta 🤔?

    Mục đích của SEO là giúp các công cụ tìm kiếm nhìn thấy trang web. Khi người dùng tìm kiếm nội dung mà có liên quan thì nó hiển thị trang web của bạn ra kết quả tìm kiếm. Điều này gần như là đôi bên cùng có lợi khi Google giúp nhiều người biết đến trang web hơn, còn Google thì được nhiều người dùng hơn.

    Bây giờ cuộc chơi với các AI Agents thì lại khác. AI Agents phải chủ động đi tìm kiếm nguồn thông tin và tiện thể "cào" luôn dữ liệu của bạn về, rồi xào nấu hay làm gì đó mà chúng ta cũng chẳng thể biết được. Vậy đây gần như là cuộc chơi chỉ mang lại lợi ích cho 1 bên 🤔!?

    Nước đi của CloudFlare là bắt AI Agents phải trả tiền cho mỗi lần lấy dữ liệu từ trang web của bạn. Nếu không trả tiền thì tôi không cho ông đọc dữ liệu của tôi. Kiểu vậy. Hãy chờ thêm một thời gian nữa xem sao 🤓.

    » Xem thêm
  • Lúc khái niệm "Vibe Code" bùng nổ mình cũng tò và tìm hiểu xem nó là gì. Hoá ra là chỉ cách lập trình mới: Lập trình viên ra lệnh và để cho LLM tự viết mã. Sau đó là hàng loạt các bài viết nói về cách họ đã xây dựng ứng dụng mà không cần phải viết một dòng mã nào, hoặc 100% là do AI viết...

    Mình không có ý kiến gì vì mỗi người một sở thích. Nhưng nếu tiếp xúc với nhiều thông tin như vậy thì ít nhiều thế hệ lập trình viên mới sẽ "ám ảnh". Khi làm việc với ngôn ngữ lập trình, chúng ta đang tiếp xúc ở bề nổi rồi. Đằng sau đó còn nhiều lớp khác che giấu sự phức tạp. Ví dụ biết viết JavaScript nhưng có biết nó chạy như thế nào không 🤔? Trên thực tế bạn chẳng cần phải biết nó chạy như thế nào mà chỉ cần biết cú pháp là viết được chương trình chạy ngon ơ.

    LLMs giờ đây lại thêm một lớp ảo hoá cho việc viết mã. Tức là nơi chúng ta không cần trực tiếp viết mà là ra lệnh. Làm việc sẽ nhanh hơn nhưng khi gặp vấn đề thì nhiều khả năng phải vận dụng kiến thức của tầng thấp hơn để giải quyết.

    Mình dùng Cursor, nhưng tính năng thích nhất và dùng nhiều nhất là Autocomplete & Suggestions. Thi thoảng cũng dùng Agents để bảo nó viết tiếp đoạn mã đang dở, thường thì nó làm rất tốt. Hoặc khi gặp lỗi thì hỏi, có lúc giải quyết được, lúc thì không. Nhìn chung nó đang làm thay nhiệm vụ của Google & Stack Overflow, giúp tiết kiệm thời gian 😆

    LLMs như một cuốn bách khoa toàn thư rất khủng khiếp. Hỏi gì cũng biết, cũng trả lời được nhưng có một sự thật là nó chỉ là mô hình đoán chữ (đoán tokens). Thế nên nếu vấn đề phổ biến thì nó sẽ làm rất tốt, nhưng vấn đề ít phổ biến hơn thì nó lại rất tệ, hoặc thậm chí là đưa ra thông tin sai lệch, nhiễu... Tóm lại, cần phải biết cách khai thác thông tin, mà để biết thì buộc người dùng phải có một lượng kiến thức nhất định, tránh rơi vào cái bẫy thiên kiến uy quyền (tin tưởng tuyệt đối vào ai đó) hoặc thiên kiến xác nhận (xác nhận niềm tin sẵn có bằng cách chỉ tìm bằng chứng xác nhận niềm tin đó).

    Tại thấy bài viết này nên lại nổi hứng viết vài dòng 🤓 Why I'm Dialing Back My LLM Usage

    » Xem thêm

Vấn đề

1 năm trước, tôi đã hô hào rằng sẽ quyết tâm học Rust trong vòng 1 tháng. Và kết quả thì như bạn đã thấy, chuỗi bài viết về quá trình học Rust vẫn chưa kết thúc. Như vậy có thể coi đó là một sự thất bại rồi phải không? Không biết bạn đọc nghĩ sao nhưng tôi thì nghĩ không hẳn là như thế.

Ngôn ngữ lập trình suy cho cùng là một công cụ để giải bài toán. Biết thêm được một cái mới, hẳn kinh nghiệm làm bài sẽ nhiều hơn, chưa kể còn giải được theo cách tối ưu. Bên cạnh việc học, tôi cũng có nhiều việc phải làm nữa. Những công việc đó có độ ưu tiên cao hơn nên buộc mình phải bắt tay vào làm sớm hơn.

Nhiều lần tôi có ý thôi không học nữa. Nhưng bằng một lý do nào đó mà chuỗi bài viết về Rust vẫn được bạn đọc yêu thích. Tôi không rõ tại sao, vì gần như những gì viết ra chỉ là các bản tóm tắt hoặc có thêm đôi ba lời giải thích cách hiểu của mình về Rust. Thậm chí còn không được đầy đủ so với tài liệu gốc (dù sao thì tôi vẫn khuyên bạn nên đọc bản gốc 😅). Có vẻ như thứ mà nhiều người muốn thấy là một người đồng hành. À! Ít ra thì đang có người học Rust giống như mình.

Một năm vừa rồi, chúng ta hầu như đã học được gần hết kiến thức cơ bản của Rust, về biến, kiểu dữ liệu và cả một số cấu trúc dữ liệu cơ bản. Riêng phần khó nhất - quyền sở hữu thì tôi tin là phải bắt tay vào làm dự án thực sự mới phát sinh ra nhiều vấn đề, chứ mà lý thuyết thì ai mà chẳng đọc được đúng không.

Vậy thì đây cũng là thời điểm để làm một bài thực hành, ứng dụng những gì đã học được để tạo một công cụ dòng lệnh (CLI). Nhưng đến đây tôi chợt nhớ ra trước đó mình cũng có tạo ra một ứng dụng CLI để tạo và upload hình ảnh thumbnail trong bài viết Ứng dụng CLI để tăng hiệu suất trong công việc. Sẽ thật tuyệt vời nếu viết lại được công cụ này bằng Rust.

Ứng dụng trước đó đang giải quyết công việc xử lý kích thước và tải lên ảnh hàng loạt các bài viết trên blog. Tưởng tượng bạn có một hình ảnh, bất kể định dạng nào, kích thước bao nhiêu thì khi đưa vào CLI này nó sẽ tạo ra một loạt các hình ảnh với kích thước khác nhau rồi tải chúng lên R2 của Cloudflare. Thứ chúng ta nhận được sau đó là tất cả đường dẫn đến hình ảnh ở trên.

Nhưng suy đi cũng phải ngẫm lại, nếu mang hết tính năng của CLI cũ sang thì mất kha khá thời gian đấy, cho nên trong một bài viết ngắn này, tôi sẽ đơn giản đi một chút. Bỏ qua công đoạn xử lý ảnh đi, giờ chỉ cần nhập vào một hình ảnh thì nó tải lên R2 thôi.

Bắt đầu nào!

Đầu tiên tạo một project Rust mới bằng cargo, đặt tên là img-cli.

$ cargo new img-cli

Chạy thử chương trình.

$ cargo run
   Compiling img-cli v0.1.0 (/Users/hoaitx/src/hoaitx/img-cli)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.35s
     Running `target/debug/img-cli`
Hello, world!

Bây giờ chúng ta cần lấy được tham số là đường dẫn đến hình ảnh cần được tải lên. Một cái gì đó giống như là:

$ img-cli path_to_image

Thì trong Rust cách đơn giản nhất để lấy các tham số đằng sau lệnh là args.

fn main() {
    let args: Vec<String> = env::args().collect();
    let file_path = &args[1];
}

API tải ảnh lên R2 gọi qua PUT, với body là binary chứa dữ liệu ảnh cần tải lên. Một điều cần lưu ý là tên của ảnh tải lên R2 được trích xuất từ đường dẫn API. Ngoài ra, cần truyền thêm một tham số bí mật là 'X-Custom' trong headers để xác thực quyền tải lên hình ảnh.

Nhưng trước tiên, viết một hàm để đọc dữ liệu file ảnh.

fn read_file(path: &str) -> Result<Vec<u8>, std::io::Error> {
    let mut file = File::open(path)?;
    let mut file_bytes = Vec::new();
    file.read_to_end(&mut file_bytes)?;
    Ok(file_bytes)
}

read_file nhận vào một đường dẫn đến hình ảnh, trả về dữ liệu kiểu Vec<u8> nếu thành công. Ngược lại, nó sẽ trả về lỗi. Vec<u8> là kiểu dữ liệu được dùng trong body của API.

Tiếp theo, viết một hàm tải ảnh lên R2. Ở đây tôi sử dụng thư viện reqwest để gọi API. Thư viện này thường đi kèm với tokio để xử lý bất đồng bộ. Tokio là một runtime cung cấp môi trường xử lý bất đồng bộ cho Rust. Để đơn giản, chúng ta sẽ tạm thời không dùng đến mà chỉ sử dụng API blocking của reqwest.

Cài đặt reqwest:

$ cargo add reqwest --features blocking

Lưu ý là sau khi gọi API, kết quả trả về là một JSON có chứa liên kết đến hình ảnh vừa tải lên. Chúng ta cần lấy được liên kết này và in ra màn hình. Để làm được cần phải tiến hành mapping được các trường dữ liệu vào một kiểu dữ liệu struct. Cài thêm 2 thư viện serdeserde_json để xử lý.

$ cargo add serde --features derive
$ cargo add serde_json

Khai báo một struct Response để mapping phản hồi của API.

#[derive(serde::Deserialize, serde::Serialize)]
struct Response {
    full: String,
}

Ráp chúng lại với nhau thành một hàm up_file.

#[derive(serde::Deserialize, serde::Serialize)]
struct Response {
    full: String,
}

fn up_file(path: &str) -> Result<String, Box<dyn std::error::Error>> {
    let url = "https://static-img.2coffee.dev";
    let client = reqwest::blocking::Client::new();
    let file_path = path;

    // lấy ra tên file chính là chuỗi cuối cùng sau dấu '/'
    let file_name = file_path
        .split('/')
        .last()
        .unwrap_or("unknown")
        .to_string();

    let file_bytes = read_file(file_path)?;

    let response = client
        .put(format!("{}/{}", url, file_name))
        .header("Content-Type", "application/x-binary")
        .header("X-Custom-Auth-Key", "XXX")
        .body(file_bytes)
        .send();

    let response_struct: Response =
        serde_json::from_str(response?.text()?.as_str())?;

    Ok(response_struct.full)
}

Đưa các hàm vào main.

fn main() {
    let args: Vec<String> = env::args().collect();
    let file_path = &args[1];
    let url = up_file(file_path);

    print!("{}", url.unwrap());
}

Chạy thử

$ cargo run -- '/Users/hoaitx/Downloads/hoaitx.jpg'
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.20s
     Running `target/debug/rs /Users/hoaitx/Downloads/hoaitx.jpg`
https://static-img.2coffee.dev/hoaitx.jpg

Mọi thứ hoạt động!

Trong bài viết này chúng ta đã vận dụng những kiến thức cơ bản và thêm cả cách sử dụng một số thư viện để gọi API cũng như xử lý kết quả phản hồi. Quả là khi bắt tay vào làm thì mới thấy khó khăn. Các bài viết sau đó, chúng ta sẽ tiến dần tới nhiều khái niệm nâng cao trong Rust. Hy vọng mọi người sẽ tiếp tục ủng hộ series bài viết về Rust này trong tương lai!

Cao cấp
Hello

Bí mật ngăn xếp của Blog

Là một lập trình viên, bạn có tò mò về bí mật công nghệ hay những khoản nợ kỹ thuật về trang blog này? Tất cả bí mật sẽ được bật mí ngay bài viết dưới đây. Còn chờ đợi gì nữa, hãy bấm vào ngay!

Là một lập trình viên, bạn có tò mò về bí mật công nghệ hay những khoản nợ kỹ thuật về trang blog này? Tất cả bí mật sẽ được bật mí ngay bài viết dưới đây. Còn chờ đợi gì nữa, hãy bấm vào ngay!

Xem tất cả

Đăng ký nhận thông báo bài viết mới

hoặc
* Bản tin tổng hợp được gửi mỗi 1-2 tuần, huỷ bất cứ lúc nào.

Bình luận (0)

Nội dung bình luận...