+1

[Rust/VBA] Cách sử dụng Rust từ VBA - Phần 1

[Rust/VBA] Cách sử dụng Rust từ VBA - Phần 1

Mục lục

  • Tổng quan
  • Mục đích
  • Kiến trúc và Giải thích khái niệm
  • Thực hành1 Truyền nhận các kiểu nguyên thủy
  • Thực hành2 Truyền nhận chuỗi ký tự(String)

Tổng quan

Xin chào! Đây là bài đăng đầu tiên của tôi. Tôi là người Nhật, nhưng vì vợ tôi là người Việt Nam nên tôi quyết định đăng bài lên đây để học tiếng Việt. Tôi đang sử dụng AI để hỗ trợ dịch thuật, nên có thể sẽ có những chỗ không tự nhiên hoặc ý nghĩa chưa được chính xác, mong mọi người thông cảm và giúp đỡ.

Đối với bài đăng đầu tiên thì nội dung này có vẻ hơi "ngách" (niche) và nhu cầu có lẽ không cao lắm... Tôi xin giới thiệu một kỹ thuật để gọi các xử lý của Rust từ VBA và sử dụng chúng như một phần của VBA. Bài này không nói về việc chỉnh sửa file xlsx tĩnh, nên nếu bạn quan tâm đến việc đó, hãy tìm kiếm các crate như rust_xlsxwriter nhé.

Mục-đích

Gần đây Excel đã có các dịch vụ hỗ trợ Python như Python in Excel hay xlwings, nhưng chúng có những hạn chế nhất định so với VBA như phải trả phí, chạy trên cloud (khiến dữ liệu bị gửi ra ngoài công ty), hoặc yêu cầu máy người nhận file macro phải cài đặt môi trường Python.

Ngoài ra, việc thao tác file Excel tĩnh từ bên ngoài bằng openpyxl hay pandas cũng được, nhưng thay đổi sẽ không phản ánh ngay lập tức khi Excel đang mở mà thông qua một quy trình tĩnh, nên tùy vào tình huống vận hành mà cách này có thể không phù hợp.

Vì vậy, mục đích chính của bài viết này là bù đắp nhược điểm về hiệu năng (tốc độ) của VBA bằng cách gọi file dll từ VBA để đảm nhận các logic xử lý nặng. Nếu chỉ là tính toán đơn thuần thì kỹ thuật này có vẻ không cần thiết lắm, nhưng ví dụ như việc triển khai 1 triệu dòng dữ liệu lên sheet, nếu áp dụng phương pháp này thì sẽ xong trong nháy mắt. Hơn nữa, dữ liệu sẽ được phản ánh ngay lập tức vào ô (cell) khi Excel đang mở. (Tuy nhiên, chúng ta sẽ chưa làm đến mức đó trong bài viết này...)

Kiến trúc và Giải thích khái niệm

VBA và Rust là hai ngôn ngữ khác nhau, nên Runtime (môi trường chạy), Quy tắc quản lý quyền bộ nhớCách sắp xếp dữ liệu hoàn toàn khác nhau. Về Runtime, Rust chạy bằng mã máy (native code) mà CPU có thể hiểu trực tiếp, trong khi VBA giống như C# hay Java, được biên dịch thành mã trung gian và chạy trên máy ảo (tuy cơ chế có khác nhau). Quyền quản lý bộ nhớ liên quan đến việc có GC (Garbage Collection) hay không (nói chính xác thì VBA không có GC mà dùng cơ chế đếm tham chiếu - reference counting). Trong các ngôn ngữ có GC, quyền quản lý bộ nhớ thuộc về GC, và GC có thể tự ý thay đổi địa chỉ bộ nhớ. Về Cách sắp xếp dữ liệu, VBA giống như Python hay các ngôn ngữ bậc cao khác, một dữ liệu mang theo rất nhiều metadata (siêu dữ liệu), khác với cách sắp xếp dữ liệu đơn giản của Rust hay C.

Để giải quyết sự khác biệt này, chúng ta cần làm cho cả hai có thể giao tiếp theo một quy tắc chung. Cái thực hiện điều đó chính là C ABI mà cả VBA và Rust đều hiểu. Đây là cơ chế FFI (Foreign Function Interface), và giao thức thực tế được sử dụng là C ABI.

ABI (Application Binary Interface) rất giống với API (Application Programming Interface) (chỉ khác một chữ cái), nhưng ABI là quy ước về cách sắp xếp các số 0 và 1, nên nó là câu chuyện ở tầng thấp hơn API.

C ABIABI dành cho ngôn ngữ C, nhưng trong lịch sử phát triển, nó đã trở thành tiêu chuẩn chung cho mọi ngôn ngữ. Có thể nói nó giống như "Tiếng Anh" trong thế giới lập trình vậy. Tóm lại, VBA và Rust sẽ tuân theo quy tắc chung này để đối thoại với nhau.

Kiến trúc

Ở trên tôi đã nói hơi sâu về lý thuyết, nhưng cấu trúc thực tế khá đơn giản. Phía VBA có thể viết code với cảm giác tương tự như khi gọi Windows API từ VBA.

  1. Khai báo hàm nào của dll sẽ được sử dụng bằng từ khóa Declare (thường thấy khi dùng Windows API trong VBA).
  2. Khi VBA truyền dữ liệu, nó chuyển đổi sang kiểu dữ liệu nguyên thủy tương thích với C.
    ※ Các số như Long có thể truyền trực tiếp, nhưng chuỗi hoặc object nằm trên Heap thì sẽ truyền con trỏ (pointer).
  3. Phía Rust nhận dữ liệu, nếu là con trỏ thì sẽ đọc dữ liệu tại địa chỉ đó theo quy tắc của C và chuyển đổi sang kiểu nguyên thủy của Rust.
  4. Khi Rust trả dữ liệu về, nó chuyển đổi thành con trỏ hoặc số theo quy tắc của C.
  5. Phía VBA nhận con trỏ đó và chuyển đổi sang kiểu nguyên thủy của VBA.

Hình trên mô tả quy trình truyền dữ liệu từ VBA sang Rust thông qua C ABI.

Cơ bản

Cấu trúc dự án

Vì chúng ta sẽ tạo file dll, hãy tạo dự án bằng lệnh cargo new project-name --lib. Trong file Cargo.toml, bắt buộc phải thêm đoạn sau:

[lib]
crate-type = ["cdylib"]

cdylib là viết tắt của "C Dynamic Library". Chỉ định cái này giúp tạo ra file dll mà VBA hay Python có thể gọi được. Nếu không chỉ định, nó sẽ tạo ra file thư viện chuyên dụng cho Rust là .rlib.

Cú pháp cơ bản

Khi trao đổi dữ liệu giữa VBA và Rust, phía Rust cần tuân thủ 3 cú pháp cơ bản sau:

  • extern "system" Đây là quy ước khi gọi dll, chỉ định ai sẽ là người chịu trách nhiệm dọn dẹp hàm sau khi chạy xong. Bạn cứ coi nó như một template bắt buộc phải có là được.

    extern "system" fn add(a: i32, b: i32) -> i32 {
        a + b
    }
    

    Trên Windows 64bit hiện đại thì không cần quá bận tâm, nhưng nếu là Windows 32bit, nếu bạn dùng extern "C" thì phía VBA sẽ phải tự dọn dẹp bộ nhớ, rất phiền phức. Nếu liên kết với VBA, cơ bản dùng system là phù hợp nhất.

  • #[unsafe(no_mangle)] Vô hiệu hóa Name Mangling. Khi biên dịch, Rust thường thêm các giá trị hash vào tên hàm để đảm bảo tính duy nhất. Nếu để nguyên thì VBA sẽ không tìm thấy tên hàm, nên ta cần thêm thuộc tính này để giữ nguyên tên hàm.

    #[unsafe(no_mangle)]
    extern "system" fn add(a: i32, b: i32) -> i32 {
        a + b
    }
    

    Lúc này tên hàm add có thể được dùng y nguyên trong VBA.

    ※ Thuộc tính unsafe trong no_mangle được thêm vào từ Rust 1.82. Ở các phiên bản trước, nếu không dùng #[no_mangle] thì có thể bị lỗi.

  • #[repr(C)] Đây là thuộc tính để chuyển đổi struct hoặc enum của Rust sang dạng của C. Nếu không có cái này, bố cục bộ nhớ (memory layout) của struct/enum trong Rust sẽ khác với C, dẫn đến việc VBA không thể truy cập chính xác.

    #[repr(C)]
    struct MyStruct {
        a: i32,
        b: f64,
    }
    

Thực hành1 Truyền nhận các kiểu nguyên thủy

Đầu tiên, chúng ta bắt đầu với việc truyền nhận số (số nguyên, số thực). Đây là kiểu "truyền tham trị" (pass-by-value), sao chép giá trị trực tiếp trên bộ nhớ để truyền đi, nên không phức tạp về quản lý bộ nhớ và là cách liên kết an toàn nhất.

Bảng đối chiếu kiểu dữ liệu VBA và Rust

Giữa VBA và Rust, dù cùng là "số nguyên" nhưng số bit có thể khác nhau. Nếu nhầm lẫn chỗ này, kết quả tính toán sẽ sai hoặc Excel có thể bị crash (đóng đột ngột). Đặc biệt cần lưu ý: Long của VBA là 32-bit (không phải 64-bit).

Kiểu VBA Số bit Kiểu Rust tương ứng Ghi chú
Byte 8-bit u8 0 ~ 255
Integer 16-bit i16 -32,768 ~ 32,767
Long 32-bit i32 Hay dùng nhất
LongLong 64-bit i64 ※Chỉ dành cho Excel 64bit
Single 32-bit f32 Số thực (độ chính xác đơn)
Double 64-bit f64 Số thực (độ chính xác kép)
Boolean 16-bit i16 True trong VBA là -1 nên nhận về dạng số sẽ an toàn hơn

Thử tạo hàm cộng

Chúng ta sẽ tạo một hàm nhận vào 2 số nguyên và thực hiện phép cộng.

#[unsafe(no_mangle)]
pub extern "system" fn rust_add(a: i32, b: i32) -> i32 {
    a + b
}

Hãy chạy cargo build. ※ Trên Windows 32bit lệnh build sẽ khác, nhưng bài này tôi sẽ nói về Windows 64bit đang phổ biến hiện nay.

Sau khi build xong, file dll sẽ được tạo ra trong thư mục target/debug/ của dự án. Nếu build release thì nó nằm trong target/release/. Hãy copy đường dẫn tuyệt đối của file dll này.

Tiếp theo là xử lý phía VBA. Viết vào module như sau:

' Chỉ định đường dẫn dll
#If Win64 Then
  Private Declare PtrSafe Function rust_add Lib "C:\my_rust_lib.dll" (ByVal a As Long, ByVal b As Long) As Long
#End If

Sub TestRustAdd()
    Dim result As Long
    ' Gọi hàm của Rust
    result = rust_add(10, 20)

    ' Kiểm tra kết quả: Nếu hiện 30 là thành công!
    MsgBox "Calc Result: " & result
End Sub

Ở dòng Declare đầu tiên, ta chỉ định đường dẫn dll để gọi hàm và khai báo tên hàm sẽ dùng trong nội bộ VBA. Sau đó thì cứ dùng như một hàm VBA bình thường thôi.

Bạn có thấy kết quả hiển thị như hình dưới không? Những dữ liệu đơn giản nằm trên stack memory như thế này thì giá trị được truyền trực tiếp nên không khó.

Thử xem qua kiểu bool nhé.

#[unsafe(no_mangle)]
pub extern "system" fn rust_judge(bool_raw: i32) -> i32 {
    let bool_val: bool = bool_raw == -1;
    // Nếu true trả về 10, false trả về 20.
    if bool_val { 10 } else { 20 }
}

True được truyền vào dưới dạng số là -1.

Phía VBA sửa lại như sau:

' Chỉ định đường dẫn dll
#If Win64 Then
  Private Declare PtrSafe Function rust_judge Lib "C:\my_rust_lib.dll" (ByVal bool_raw As Long) As Long
#End If

Sub TestRustAdd()
    Dim result As Long
    ' Gọi hàm Rust
    result = rust_judge(True)

    ' Kiểm tra kết quả: Hiện 10 là thành công!
    MsgBox "Rust Result: " & result
End Sub

Bạn có thấy kết quả như hình dưới không?

Ở đây cần chú ý là trong câu lệnh Declare của VBA, nhất định phải chỉ định ByVal (truyền tham trị). Nếu không có ByVal, nó sẽ trở thành ByRef (truyền tham chiếu) và sẽ truyền đi con trỏ. Tức là phía Rust định đọc dữ liệu số, nhưng thực tế lại nhận được địa chỉ của stack memory chứa số đó.

Thực hành2 Truyền nhận chuỗi ký tự (String)

Kiểu dữ liệu chuỗi trong các ngôn ngữ bậc cao thì xử lý dễ dàng như số, nhưng trong ngôn ngữ cấp thấp hoặc khi xử lý gần phần cứng, nó bỗng trở thành một sự tồn tại rất phiền toái. Thực tế trong Rust, khi bắt đầu phải để ý đến quyền sở hữu (ownership) cũng là từ chuỗi ký tự mà ra...

Vấn đề về cách truyền dữ liệu

Nếu bạn từng chạm vào ngôn ngữ cấp thấp, bạn sẽ biết số đơn thuần và chuỗi ký tự được đặt ở các vùng nhớ khác nhau. Các loại số cơ bản được đặt ở Stack memory tốc độ cao, và khi gán sang biến khác thì nó được copy.

let s = 1;
let s2 = s; // Copy
println!("s vẫn dùng được. {}", s);

Tuy nhiên, kiểu chuỗi được cấp phát trên Heap memory (chậm hơn nhưng chứa được nhiều). Khi gán sang biến khác, cơ bản là truyền tham chiếu (reference). Như code dưới đây:

let s = String::from("Hello");
let s2 = s; // Di chuyển quyền sở hữu (Cấp phát vùng nhớ mới trên Stack, copy địa chỉ con trỏ mà s đang giữ -> s bị vô hiệu)
println!("s không dùng được nữa. {}", s); // Lỗi biên dịch

Đây gọi là Shallow Copy (Sao chép nông) vì nó không copy dữ liệu thực tế mà chỉ copy địa chỉ con trỏ. Rust xử lý việc này bằng quy tắc Ownership (Quyền sở hữu), nhưng trong C hay C++ thì tư duy cũng tương tự. Không chỉ có vấn đề về vị trí bộ nhớ, vấn đề Encoding (mã hóa ký tự) cũng không thể bỏ qua.

Vấn đề Encoding

VBA sử dụng UTF-16LE, tiếng Nhật hay tiếng Anh hay tiếng Việt cơ bản đều được biểu diễn bằng 2 byte. Rust sử dụng UTF-8 quen thuộc, tiếng Anh là 1 byte, tiếng Nhật/Việt là khoảng 3 byte (độ dài thay đổi). Nếu truyền chuỗi mà không giải quyết sự khác biệt này sẽ gây lỗi font (mojibake) hoặc vi phạm truy cập bộ nhớ (crash).

Cách đơn giản nhất để giải quyết là truyền ByVal s as String trong VBA. Khi đó, hệ thống sẽ tự động chuyển đổi chuỗi theo System Locale sang định dạng C (CStr) rồi truyền đi. Phía Rust chỉ cần chuyển từ đó sang UTF-8. Tuy nhiên, cách này phụ thuộc vào System Locale của máy tính, nên nếu chạy trên PC có Locale khác thì rủi ro lỗi font rất cao.

Vì vậy, cách chắc chắn nhất là lấy nguyên chuỗi UTF-16 chuẩn của VBA, rồi tự chuyển sang UTF-8 bên trong Rust. Lúc này, từ VBA ta không truyền chuỗi, mà truyền địa chỉ bộ nhớ và độ dài dữ liệu trực tiếp sang Rust, để Rust khôi phục lại chuỗi UTF-16. Đây là điểm khó hơn hẳn so với việc truyền số đơn giản.

Thực hành - Rust tham chiếu bất biến (immutable reference) đến bộ nhớ do VBA cấp phát

Phía Rust sẽ cài đặt như sau:

use ::std::slice;

#[unsafe(no_mangle)]
pub extern "system" fn count_chars(ptr: *const u16, len: i32) -> i32 {
    if ptr.is_null() {
        return 0;
    }

    // Khôi phục string slice từ con trỏ và độ dài
    let utf16_slice = unsafe { slice::from_raw_parts(ptr, len as usize) };

    // Chuyển từ UTF-16 sang chuỗi Rust (UTF-8)
    let rust_string = String::from_utf16_lossy(utf16_slice);

    // Trả về số ký tự
    rust_string.chars().count() as i32
}

Phía VBA cài đặt như sau. Truyền con trỏ và độ dài dữ liệu. Lần này tôi dùng Alias "Tên_hàm_Rust" để đặt một tên khác cho hàm dùng trong VBA.

' Chỉ định đường dẫn dll
# if Win64 Then
  Private Declare PtrSafe Function rust_count_chars Lib "C:\my_rust_lib.dll" Alias "count_chars" (ByVal ptr As LongPtr, ByVal length As Long) As Long
#End If

Sub TestRustAdd()
    Dim s As String
    s = "Hello"

    ' Truyền con trỏ dữ liệu chuỗi và số lượng ký tự
    Dim result As Long
    result = rust_count_chars(StrPtr(s), Len(s))

    MsgBox "String Count: " & result
End Sub

Kết quả sẽ hiển thị như hình dưới.

Trong code này, Rust chỉ đang "nhìn trộm" dữ liệu mà VBA đã mở trên Heap memory dưới dạng slice, nên dù Rust có dùng chuỗi đó thì quyền sở hữu bộ nhớ vẫn không chuyển sang Rust. Tức là, VBA vẫn quản lý vùng nhớ này. Khi biến s (đang giữ tham chiếu đến Xin chào) ra khỏi phạm vi ở End Sub và bị giải phóng, thì dữ liệu Xin chào trên Heap cũng sẽ được giải phóng theo.

Thực hành - VBA tham chiếu đến bộ nhớ do Rust cấp phát

Tiếp theo tôi muốn thử truyền chuỗi từ Rust về VBA. Chỗ này cần chú ý một chút. Đừng chạy code dưới đây vội nhé.

use std::ffi::CString;

#[unsafe(no_mangle)]
pub extern "C" fn get_message() -> *const c_char {
    let s = CString::new("Hello form Rust").unwrap();

    s.into_raw()
}

Ở trên, phía Rust dùng CString để mở chuỗi trên Heap. Dùng phương thức into_raw để trả về con trỏ cho VBA. into_raw là phương thức từ bỏ quyền quản lý bộ nhớ và trả về con trỏ. Phía VBA sẽ cần xử lý để nhận cái này dưới dạng UTF-8, nhưng vấn đề lớn hơn là: Dữ liệu Hello form Rust sẽ không được giải phóng ngay cả khi hàm get_message kết thúc. Tức là nếu cứ để vậy sẽ bị Memory Leak (rò rỉ bộ nhớ).

Vì vậy, cần tạo một hàm giải phóng bộ nhớ bên phía Rust, và phía Excel sau khi dùng xong bắt buộc phải gọi hàm đó. Cài đặt phía Rust như sau. Bao gồm hàm thông thường và hàm giải phóng bộ nhớ cho CString.

use std::{ffi::CString, os::raw::c_char};

#[unsafe(no_mangle)]
pub extern "C" fn get_message() -> *const c_char {
    let s = CString::new("Hello form Rust").unwrap();
    s.into_raw() // Rust từ bỏ quản lý bộ nhớ & trả về con trỏ
}

// Hàm chuyên dụng để giải phóng bộ nhớ chuỗi
#[unsafe(no_mangle)]
pub extern "system" fn free_string(s: *mut c_char) {
    // Ngăn chặn giải phóng bộ nhớ 2 lần (Double Free)
    if s.is_null() {
        return;
    }
    unsafe {
        // Khôi phục CString từ con trỏ, và để nó tự giải phóng khi ra khỏi scope
        let _ = CString::from_raw(s);
    }
}

Phía VBA sẽ đọc bộ nhớ do Rust cấp phát, chuyển đổi sang kiểu String của VBA để sử dụng, sau đó chạy free_string của Rust để giải phóng bộ nhớ. Trong quá trình đó sẽ chèn thêm xử lý chuyển đổi từ UTF-8 sang UTF-16, hãy dùng hàm Utf8PtrToString bên dưới. Hàm này dùng Windows API nên phần Declare sẽ nhiều hơn chút.

Option Explicit

' ==================================================
' Declare
' ==================================================
' Định nghĩa hàm phía Rust
#If Win64 Then
    ' Rust API
    Private Declare PtrSafe Function rs_get_message Lib "C:\my_rust_lib.dll" Alias "get_message" () As LongPtr
    Private Declare PtrSafe Sub rs_free_string Lib "C:\my_rust_lib.dll" Alias "free_string" (ByVal ptr As LongPtr)

    ' Windows API
    Private Declare PtrSafe Function lstrlen Lib "kernel32" Alias "lstrlenA" (ByVal lpString As LongPtr) As Long
    Private Declare PtrSafe Function lstrlenA Lib "kernel32" (ByVal lpString As LongPtr) As Long
    Private Declare PtrSafe Function MultiByteToWideChar Lib "kernel32" ( _
        ByVal CodePage As Long, _
        ByVal dwFlags As Long, _
        ByVal lpMultiByteStr As LongPtr, _
        ByVal cbMultiByte As Long, _
        ByVal lpWideCharStr As LongPtr, _
        ByVal cchWideChar As Long _
    ) As Long
#End If

' ==================================================
' CONSTANTS
' ==================================================
' Mã code page cho UTF-8
Private Const CP_UTF8 As Long = 65001


' ==================================================
' Function
' ==================================================
Sub GetMessageFromRust()
    ' Khởi tạo (cho chắc)
    Dim ptr As LongPtr
    ptr = 0

    On Error GoTo Finally

    ' Lấy con trỏ (Rust cấp phát bộ nhớ)
    ptr = rs_get_message()

    ' Nếu null thì kết thúc
    If ptr = 0 Then GoTo Finally

    ' Chuyển đổi & Copy sang String của VBA
    ' ※ Lúc này nội dung bộ nhớ được sao chép sang vùng nhớ của VBA
    Dim strVal As String
    strVal = Utf8PtrToString(ptr)

    MsgBox strVal


Finally:
    If ptr <> 0 Then
        Call rs_free_string(ptr)
        ptr = 0 ' Xóa đi để tránh giải phóng 2 lần
    End If

    ' Nếu có lỗi xảy ra thì thông báo và kết thúc
    If Err.Number <> 0 Then
        MsgBox "An error occurred: " & Err.Description, vbCritical
    End If

End Sub


' ==================================================
' Util Function
' ==================================================
' Thêm mới: Hàm chuyển đổi Con trỏ UTF-8 -> Chuỗi VBA
Public Function Utf8PtrToString(ptr As LongPtr) As String
    Dim strlen As Long
    Dim bufSize As Long
    Dim buffer() As Byte

    If ptr = 0 Then Exit Function

    ' 1. Lấy độ dài byte của chuỗi UTF-8 (đến ký tự null)
    strlen = lstrlenA(ptr)
    If strlen = 0 Then Exit Function

    ' 2. Tính số ký tự UTF-16 sau khi chuyển đổi (truyền 0 để lấy kích thước bộ đệm trước)
    bufSize = MultiByteToWideChar(CP_UTF8, 0, ptr, strlen, 0, 0)

    If bufSize > 0 Then
        ' 3. Cấp phát bộ đệm (String của VBA là UTF-16, nên chỉ cần lấp đầy bộ đệm là dùng được như String)
        Utf8PtrToString = String$(bufSize, vbNullChar)

        ' 4. Thực hiện chuyển đổi và ghi vào vùng nhớ String của VBA
        ' Dùng StrPtr để truyền địa chỉ bộ nhớ của biến chuỗi
        Call MultiByteToWideChar(CP_UTF8, 0, ptr, strlen, StrPtr(Utf8PtrToString), bufSize)
    End If
End Function

Trong VBA không có cú pháp try{} catch{} nhưng bằng cách đặt nhãn (label), ta có thể thiết lập để luôn chạy phần giải phóng bộ nhớ dù có lỗi. Tuy nhiên, lưu ý là nếu bạn đặt Breakpoint (điểm dừng) rồi dừng chương trình ngang chừng khiến rs_free_string không được chạy, thì bộ nhớ sẽ không được giải phóng và vẫn còn đó.

Nếu thành công sẽ hiện ra như hình dưới.

Phương pháp trên rất nhạy cảm vì nếu quên gọi hàm giải phóng bộ nhớ là sẽ bị rò rỉ bộ nhớ ngay.

Thực hành - Rust ghi dữ liệu vào bộ nhớ do VBA cấp phát (Truyền Buffer)

Vì vậy, tiếp theo tôi xin giới thiệu phương thức: Rust ghi dữ liệu trực tiếp vào vùng nhớ mà VBA đã cấp phát. Vì VBA nắm quyền sở hữu bộ nhớ nên việc giải phóng bộ nhớ sẽ do cơ chế đếm tham chiếu của VBA tự thực hiện. Trong thực tế công việc, tôi nghĩ cách này sẽ được dùng nhiều hơn và tôi cũng khuyến khích dùng cách này. Vì nó an toàn, không lo quên giải phóng bộ nhớ...

Code phía Rust như sau. Lần này không cần hàm giải phóng.

use std::{cmp, ptr};

#[unsafe(no_mangle)]
pub unsafe extern "system" fn get_greeting_safe(buffer: *mut u16, capacity: i32) -> i32 {
    unsafe {
        // Chuỗi muốn ghi
        let message = "Hello from Rust";

        // Encode sang UTF-16 (vì VBA dùng UTF-16)
        let utf16_vec: Vec<u16> = message.encode_utf16().collect();
        let msg_len = utf16_vec.len();

        // Nếu buffer là Null hoặc size <= 0 thì trả về kích thước cần thiết rồi kết thúc
        if buffer.is_null() || capacity <= 0 {
            return msg_len as i32;
        }

        // Tính độ dài tối đa có thể ghi (lấy số nhỏ hơn giữa kích thước buffer và độ dài tin nhắn)
        let copy_len = cmp::min(msg_len, capacity as usize);

        // Thực hiện copy bộ nhớ
        ptr::copy_nonoverlapping(utf16_vec.as_ptr(), buffer, copy_len);

        // Trả về số ký tự thực tế đã ghi
        copy_len as i32
    }
}

Cài đặt phía VBA:

' Giá trị trả về là số ký tự thực tế đã được ghi
Private Declare PtrSafe Function rs_get_greeting_safe Lib "C:\my_rust_lib.dll" Alias "get_greeting_safe" (ByVal buffer As LongPtr, ByVal capacity As Long) As Long

Sub TestSafeString()
    ' Cấp phát bộ đệm (Ví dụ: dành chỗ cho 255 ký tự, lấp đầy bằng ký tự Null)
    Dim buffer As String
    buffer = String(255, vbNullChar)

    ' Gọi hàm Rust
    ' StrPtr truyền địa chỉ bắt đầu của buffer, Len truyền dung lượng
    Dim writtenLen As Long
    writtenLen = rs_get_greeting_safe(StrPtr(buffer), Len(buffer))

    ' Định dạng kết quả
    If writtenLen > 0 Then
        ' Cắt lấy phần đã được ghi từ bên trái của buffer
        Dim result As String
        result = Left(buffer, writtenLen)

        MsgBox "Result:  " & result & ",  Rust Return: " & writtenLen
    Else
        MsgBox "Write failed or buffer too small."
    End If
End Sub

Kết quả sẽ như sau:

Phương pháp này giúp code phía VBA gọn gàng hơn, lại không lo quên giải phóng bộ nhớ nên rất an toàn. Trừ khi có lý do đặc biệt, tôi nghĩ nên dùng phương thức truyền buffer này.

Về mặt code thì không quá phức tạp, nhưng vì phải vừa viết code vừa tư duy nhiều thứ trong đầu nên nó tốn "nơ-ron" hơn là truyền số liệu đơn giản.

Lần này chúng ta đã xử lý số và chuỗi, tôi dự định sẽ viết bài tiếp theo về Mảng (Array) và Cấu trúc (Struct). Nếu nắm được cách xử lý mảng, bạn hoàn toàn có thể triển khai hàng chục nghìn dòng dữ liệu vào Excel trong nháy mắt như GIF đầu bài.

Dạo gần đây AI và Machine Learning đang rất hot, nhưng thỉnh thoảng viết những dòng code "thủ công" và "gai góc" thế này cũng rất thú vị, nếu bạn thấy hứng thú thì hãy thử xem sao nhé 😎


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí