Daily Archives: 18/01/2018

Rust – Module và Crate

Trong các bài viết trước đây thì chúng ta chủ yếu viết code trong một file duy nhất, khi làm việc với những dự án thực tế thì số lượng file sẽ nhiều hơn. Có nhiều file chứa các struct, hàm… khác nhau nhưng có liên quan đến nhau, khi đo chúng ta phải gộp nhóm chúng lại với nhau. Trong Rust thì chúng ta gộp nhóm các file lại với nhau thông qua module.

Crate

Nếu bạn đã từng lập trình Java thì chắc hẳn bạn đã biết qua từ khóa package, trong Rust thì chúng ta gọi là crate.

Khi chúng ta viết các app có hàm main(), thì khi chúng ta biên dịch thì Rust sẽ tạo ra một file khả thi – tức file .exe (trên Windows) hay .sh (trên Linux) có thể chạy được.

Trong trường hợp chúng ta muốn viết các file .rs có chứa các biến, hằng, hàm, struct…. có thể được gọi từ các file khác thì khi biên dịch chúng ta sẽ có các file .dll (Windows), .so (Linux)… Đây là các file thư viện liên kết động, để project Rust biên dịch ra file này thì khi biên dịch, thay vì chúng ta đưa vào tham số --bin, chúng ta thay bằng tham số --crate-type=lib, ví dụ:

Lúc này thay vì tạo một file .exe thì Rust sẽ tạo một file .rlib, ngoài ra tên trước phần mở rộng sẽ là tên file kèm theo từ “lib”, tức ở đây chúng ta được file libmain.rlib.

Ngoài tham số lib thì chúng ta còn có thể yêu cầu Rust tạo ra các loại file thư viện khác như sau:

  • --crate-type=bin: tạo file .exe
  • --crate-type=lib: tạo file .rlib
  • --crate-type=rlib: tương tự tạo file .rlib
  • --crate-type=dylib: tạo file .dll
  • --crate-type=staticlib: tạo file .lib

Ngoài cách chỉ định trong câu lệnh rustc thì chúng ta cũng có thể chỉ định bằng cách ghi dòng này ở đầu file:

#![crate_type = "lib"]
fn main() {
println!("Hello World");
}

Module

Crate là các thực thể đã được biên dịch và phân phối trên máy tính để có thể chạy được. Mặc định Rust sẽ quy định các đoạn code một crate được “chứa” trong một module có tên là root một cách ngầm định. Sau đó các code này có thể được các coder phân chia thành các module nhỏ hơn, gọi là submodule nằm bên dưới module root.

Module cũng có thể được định nghĩa bên trong một module khác, lúc này module đó được gọi là nested. Khi biên dịch thì tất cả các module nằm trong một crate sẽ được biên dịch cùng với nhau.

Trong các bài trước chúng ta đã dùng qua một số module có sẵn trong Rust như io, str, vec… từ crate std. Crate này chứa rất nhiều module và hàm có ích trong các dự án thực tế.

Thông thường một module sẽ chứa tập hợp các đoạn code định nghĩa các trait, struct, phương thức, hàm có liên quan với nhau. Khi chúng ta đặt tên một module thì tên module đó được gọi là namespace. Để định nghĩa một module thì chúng ta dùng từ khóa mod {...}, ví dụ như sau:

mod CMS {
fn func1() { }
fn func2() { }
}

 

Tương tự với Java, mỗi file code của Rust đều được định nghĩa một module ngầm định có tên là root, kể cả khi chúng ta không khai báo từ khóa mod nào trong file. Trong đoạn code trên chúng ta định nghĩa module CMS, bên trong có 2 hàm là hàm func1()func2(). Để gọi đến 2 hàm này thì chúng ta dùng cú pháp :: như sau: CMS::func1(), CMS::fun2().

Mặc định thì các thành phần được định nghĩa bên trong một module thì chỉ có thể được gọi bên trong module đó thôi, để có thể gọi từ bên ngoài thì chúng ta phải ghi thêm từ khóa pub (viết tắt của public) trước mỗi phần tử bên trong module. Ví dụ:

mod CMS {
fn func1() {
println!("Func1 called");
}
pub fn func2() {
println!("Func2 called");
}
}

fn main() {
CMS::func1(); // lỗi
CMS::func2(); // OK
}

Lời gọi tới hàm CMS::func1() sẽ báo lỗi thông báo func1()private, còn hàm CMS::func2() thì chạy bình thường.

Func2 called

Đối với các nested module thì các thành phần bên trong đó phải được khai báo pub thì mới có thể gọi được.

Khi định nghĩa struct bên trong module thì các struct này cũng phải được khai báo với từ khóa pub thì mới có thể đọc được, tuy nhiên các phần tử của struct thì lại là private, do đó nếu chúng ta muốn đọc luôn cả các thuộc tính của struct thì chúng ta cũng phải khai báo pub cho các thuộc tính đó. Ví dụ:

pub mod CMS {
pub struct Blog {
pub name: &’static str, // public
year: i32 // private
}
}
fn main() {
let phocode = CMS::Blog {
name : "phocode.com",
year: 2017 // lỗi
};
}

Đoạn code trên sẽ báo lỗi vì thuộc tính year trong struct Blog là private và không thể đọc/ghi được.

field 'year' of struct 'CMS::Blog' is private

Import module

Chúng ta có thể sử dụng từ khóa use để chỉ định là chúng ta sẽ dùng thành phần nào trong một module mà không cần phải ghi tên module ra. Ví dụ:

use module1::fun1;
use module1::func2 as gf2;
use module2::*;
pub mod module1 {
pub fn func1() {
println!("Calling Module1.func1");        
}    
pub fn func2() {
println!("Calling Module1.func2");        
}
}
pub mod module2 {
pub fn func1() {
println!("Calling Module2.func1");        
self::func2();
}    
fn func2() {        
println!("Calling Module2.func2");    
}
}
fn main() {
module1::func1();
gf2();
func1();
}

Trong đoạn code trên, chúng ta định nghĩa 2 module là module1module2, cả 2 module này đều chứa 2 hàm là func1()func2().

Ở dòng đầu tiên chúng ta ghi câu lệnh use module1::func2 as gf2. Tức là chúng ta cho Rust biết rằng chúng ta muốn dùng hàm func2() trong module1 mà không cần phải ghi phần module1:: phía trước, bằng cách này chúng ta chỉ cần gọi func2() là được, phần as gf2 tức là chúng ta đặt tên viết tắt cho funf2 là gf2, và thay vào đó thay vì ghi func2() thì chúng ta ghi là gf2().

Nếu muốn use nhiều phần tử thì chúng ta có thể ghi như sau:

use module1::{func1, func2};

Ngoài ra trong đoạn code trên chúng ta còn có câu lệnh use module2::* có nghĩa là import toàn bộ hàm public có trong module.

Bên trong một module chúng ta có thể dùng self:: để chỉ tới module chứa hàm đó.

Calling Module1.func1
Calling Module1.func2
Calling Module2.func1
Calling Module2.func2

Import module khác file

Chúng ta có thể code các module ở trong các file khác và dùng ở một file khác. Ví dụ:

Chúng ta viết file CMSModule.rs như sau:

pub mod CMS {
pub struct Blog {
pub name: &’static str,
pub year: i32
}
}

pub fn setup() {
println!("Start setting things up");
}

File này chứa một module tên CMS và một struct là Blog.

Để có thể đọc được module này từ một file khác thì chúng ta sử dụng từ khóa mod như sau:

mod CMSModule;

fn main() {
let phocode = CMSModule::CMS::Blog{
name: "phocode.com",
year: 2017
};
println!("{}", phocode.name);
CMSModule::setup();
}

Trong đoạn code trên, câu lệnh mod CMSModule sẽ tìm tất cả các file có tên CMSModule.rs nằm trong thư mục cùng với thư mục của file hiện tại rồi đọc code trong các file đó. Nếu thư mục hiện tại không có thì Rust sẽ tiếp tục tìm trong các thư mục con cho đến hết.

phocode.com
Start setting things up

Import crate từ file khác

Như đã nói ở trên, crate là các file thư viện nhị phân được tạo ra, chứa các hàm, module, struct… đã được biên dịch và có thể sử dụng được.

Bây giờ chúng ta mở command prompt (hoặc terminal trong Linux) lên và tạo một project library có tên myLib và biên dịch thành file thư viện như sau:

cargo new myLib
cd myLib

Lệnh cargo new sẽ tạo một thư mục project có tên mylib và tạo file có tên lib.rs nằm trong thư mục src.

Chúng ta sửa file này lại như sau:

pub fn myFunc() {
println!("Calling myFunc");
}

Tiếp theo chúng ta dùng lệnh cd để di chuyển vào thư mục myLib. Sau đó chúng ta biên dịch bằng lệnh cargo build. Cargo sẽ tạo một thư mục có tên target/debug, bên trong thư mục này sẽ có một file có tên là libmyLib.rlib.

Để có thể sử dụng file thư viện này thì chúng ta sử dụng câu lệnh extern crate.

Bây giờ chúng ta tạo file main.rs trong thư mục src để sử dụng file thư viện này:

extern crate myLib;
fn main() {
myLib::myFunc();
}

Câu lệnh extern crate myLib sẽ đọc file thư viện libmyLib.rlib, và chúng ta có thể gọi tới hàm myFunc() một cách dễ dàng.

Lưu ý là câu lệnh extern chỉ đọc các thành phần public được khai báo với từ khóa pub thôi.

Bây giờ chúng ta có thể chạy lệnh cargo run để chạy file .exe từ main.rs

Calling myFunc

Có một lưu ý là chúng ta không bao giờ dùng câu lệnh extern crate std; nhưng vẫn có thể sử dụng được các hàm như println!(), lý do là vì crate std được import một cách ngầm định bởi Rust.

crates.io

Rust cũng có một cộng đồng thư viện mở rất lớn có địa chỉ tại https://crates.io, đây là nơi tập hợp rất nhiều các crate do các coder trên toàn thế giới đóng góp, chúng ta có thể tải về và sử dụng.

Để có thể tải một crate về và sử dụng thì chúng ta mở file Cargo.toml lên, sau đó thêm tên crate vào sau phần [dependencies] với cú pháp sau:

<tên_crate> = "<số_phiên_bản>"

Ví dụ, trên crates.io có một crate tên là log có chức năng giúp chúng ta debug dễ dàng, chúng ta có thể lên trang chủ và tìm kiếm với từ khóa “log”, trang web sẽ trả về danh sách các crate liên quan, chúng ta click vào để đọc, bên trong sẽ có đoạn code để chúng ta copy vào file .toml.

Ở thời điểm khi viết bài này thì crate log có phiên bản là 0.4.1, ngoài ra crate log thường hay được dùng kèm với một crate khác là env_logger, do đó chúng ta sửa lại file Cargo.toml để cài 2 crate này như sau:

[package]
name = "Example"
version = "0.1.0"
authors = ["phocode"]

[dependencies]
log = "0.4.1"
env_log = "*"

Và sau đó chúng ta chạy lệnh cargo build thì Rust sẽ tải về mã nguồn và biên dịch crate này.

> cargo build
  Blocking waiting for file lock on the registry index 
  Updating registry `https://github.com/rust-lang/crates.io-index` 
  Downloading log v0.4.1
  Downloading cfg-if v0.1.2
  Compiling cfg-if v0.1.2
  Compiling log v0.4.1
  Compiling winapi v0.2.8
  Compiling winapi-build v0.1.1
  Compiling utf8-ranges v1.0.0
  Compiling void v1.0.2
  Compiling libc v0.2.36
  Compiling lazy_static v1.0.0
  Compiling regex-syntax v0.4.2
  Compiling winapi v0.3.4
  Compiling cfg-if v0.1.2
  Compiling num-traits v0.1.41
  Compiling kernel32-sys v0.2.2
  Compiling unreachable v1.0.0
  ...

Chúng ta cũng có thể khai báo phiên bản là log = "*" thì Rust sẽ tải phiên bản mới nhất về cho chúng ta sử dụng.

Sau đó để sử dụng thì chúng ta extern crate như bình thường và gọi các hàm hoặc dùng các struct mà crate đó cung cấp, ví dụ;

#[macro_use]
extern crate log;
extern crate env_logger;

fn main() {
env_logger::init();
error!("There are some errors");
}

Trên đây chỉ là đoạn code ví dụ được mình sử dụng từ trang chủ của 2 crate này, mình sẽ không giải thích ở đây, bạn có thể tìm hiểu thêm trên mạng. Ngoài ra trong đoạn code trên chúng ta có sử dụng macro là #[macro_use], mình sẽ giải thích trong bài sau.

ERROR 2018-01-18T11:24:10Z: Test: There are some errors