[Rust/VBA] Cách gọi Rust từ VBA Phần 2 - Xử lý Mảng (Kiểu số)
[Rust/VBA] Cách gọi Rust từ VBA Phần 2 - Xử lý Mảng (Kiểu số)
Mục lục
- Mở-đầu
- Bản chất thực sự của mảng trong VBA
- Cách truyền
SafeArray1 - Chỉ truyền nội dung dữ liệu - Cách truyền
SafeArray2 - Truyền nguyên cấu trúcSafeArray
Mở đầu
Nhìn lại Phần 1
Trong [Phần 1], tôi đã giải thích cách thiết lập môi trường để gọi DLL của Rust từ VBA và cách truyền các dữ liệu cơ bản như số và chuỗi ký tự. Đối với kiểu số, vì giá trị trên Stack được copy trực tiếp để truyền đi ("Truyền tham trị" - Pass by Value) nên rất đơn giản. Tuy nhiên, đối với kiểu chuỗi, chúng ta không truyền bản thân dữ liệu mà phải truyền "Con trỏ" (Pointer) chỉ đến vị trí trên bộ nhớ Heap, nên việc xử lý có phần khó khăn hơn.
Mục tiêu của Phần 2
Mục tiêu lần này là nắm vững các khái niệm cơ bản về việc truyền nhận "xử lý mảng" cũng như cách viết code xử lý dữ liệu số, giúp cải thiện trực tiếp hiệu suất trong công việc thực tế.
Thao tác với mảng là kỹ thuật quan trọng để xử lý ngay lập tức khối lượng dữ liệu lớn ở mức "100 nghìn hay 1 triệu dòng" – điều mà VBA đơn thuần thường mất nhiều thời gian để thực hiện.
Bản chất thực sự của mảng trong VBA
Mảng trong VBA không chỉ đơn giản là các dữ liệu nằm liền kề nhau, mà là một đối tượng chứa nhiều thông tin quản lý đi kèm. Điều này cũng đúng với các ngôn ngữ lập trình bậc cao khác. Ví dụ, không chỉ trong VBA, điều gì sẽ xảy ra nếu bạn chỉ định một chỉ số (index) lớn hơn số lượng phần tử của mảng?
import numpy as np
arr = np.array([1, 2, 3])
print(arr[3]) # IndexError
Như ví dụ trên, nếu truy cập vào dữ liệu nằm ngoài phạm vi, lỗi sẽ xảy ra. Điều này quá hiển nhiên nên bạn có thể không để ý, nhưng đây chính là bằng chứng cho thấy mảng không chỉ chứa mỗi dữ liệu.
Mảng trong VBA cũng vậy. Bản chất của mảng VBA là SafeArray, một cấu trúc (struct) được định nghĩa bởi Windows OS.
Hãy cùng xem định nghĩa của cấu trúc này. Dưới đây là đoạn mã lấy từ Win32 API chính thức của Microsoft.
typedef struct tagSAFEARRAY {
USHORT cDims; // Số chiều (mảng 1 chiều là 1)
USHORT fFeatures; // Cờ (Flag - dữ liệu là con trỏ hay đang bị khóa, v.v.)
ULONG cbElements; // Số byte trên mỗi phần tử (Long là 4)
ULONG cLocks; // Số lượng khóa (cơ chế ngăn giải phóng bộ nhớ)
PVOID pvData; // Con trỏ đến vùng dữ liệu thực tế
SAFEARRAYBOUND rgsabound[1]; // Thông tin giới hạn mảng (số phần tử và chỉ số bắt đầu)
} SAFEARRAY;
※ Các comment do tôi thêm vào.
https://learn.microsoft.com/ja-jp/windows/win32/api/oaidl/ns-oaidl-safearray
Như bạn thấy, vì nó giữ cả thông tin quản lý bên cạnh dữ liệu, nên ngay cả khi truy cập ngoài phạm vi, nó có thể báo lỗi một cách an toàn.
Nếu mảng không có những metadata (siêu dữ liệu) như thế này... Lỗi sẽ không phát sinh, chương trình có thể bị crash hoặc phá hủy vùng nhớ lân cận. Nếu nhận ra lỗi ngay thì tốt, nhưng cũng có trường hợp nó âm thầm phá hoại dữ liệu. Ôi đáng sợ quá 😨
Chữ Safe trong SafeArray có nghĩa là nó có thể ngăn chặn những điều như vậy.
Sau đây, tôi sẽ giới thiệu cách truyền SafeArray này sang Rust.
Cách truyền SafeArray 1 - Chỉ truyền nội dung dữ liệu
Đầu tiên là phương pháp đơn giản. Đây là cách không truyền nguyên cả SafeArray mà chỉ truyền phần dữ liệu bên trong.
Sơ đồ khái niệm như hình dưới đây.

Phía VBA sẽ truyền Địa chỉ bắt đầu của mảng và Độ dài mảng, Rust sẽ nhận và khôi phục lại.
Ở phương pháp này, VBA và Rust chia sẻ bộ nhớ, nhưng quyền quản lý bộ nhớ vẫn thuộc về phía VBA (nơi đã cấp phát).
Nói cách khác, phía Rust sẽ xử lý dữ liệu dưới dạng tham chiếu (reference), dù là bất biến (immutable) hay khả biến (mutable).
Cách này tương tự như cách xử lý chuỗi ký tự mà chúng ta đã thực hiện ở [Phần 1].
Bây giờ hãy cùng cài đặt. Dưới đây là hàm "Nhận mảng và nhân đôi tất cả các phần tử".
use std::slice;
/// Nhận con trỏ và độ dài, thao tác trực tiếp lên nội dung mảng
#[unsafe(no_mangle)]
pub extern "system" fn process_array(ptr: *mut f64, len: i32) {
unsafe {
if ptr.is_null() || len <= 0 {
return;
}
// Tạo Slice của Rust (mutable) từ con trỏ thô (raw pointer)
// Nhờ đó có thể sử dụng các iterator tiện lợi của Rust
let slice = slice::from_raw_parts_mut(ptr, len as usize);
// Duyệt qua toàn bộ phần tử trong slice và xử lý (Ví dụ: nhân giá trị lên 1.1 lần)
for elem in slice.iter_mut() {
*elem *= 1.1;
}
}
}
Hàm nhận địa chỉ đầu tiên của dữ liệu và độ dài dữ liệu qua đối số để tạo ra một Slice. Vì không phải copy dữ liệu nên quá trình này rất nhanh.
VBA sẽ được cài đặt như sau:
#If Win64 Then
Private Declare PtrSafe Sub rs_process_array Lib "C:\my_rust_lib.dll" Alias "process_array" ( _
ByVal ptr As LongPtr, _
ByVal length As Long _
)
#End If
Sub TestArrayProcessing()
Dim i As Long
Dim count As Long
count = 1000000 ' 1 triệu phần tử
' Cấp phát mảng (Kiểu Double)
Dim data() As Double
ReDim data(count - 1)
' Gán giá trị khởi tạo để test
data(0) = 100
data(count - 1) = 200
Debug.Print "before: " & data(0) ' -> 100
' Kiểm tra xem mảng đã được khởi tạo hay chưa, hoặc có phần tử hay không
If (Not data) <> -1 Then
' Gọi Rust
Call rs_process_array(VarPtr(data(0)), count)
End If
' Xác nhận kết quả
Debug.Print "after(first): " & data(0)
Debug.Print "after(last): " & data(count - 1)
Debug.Print "Complete!"
End Sub
Kết quả sẽ như hình dưới đây.
Nếu chạy thử, bạn sẽ thấy việc xử lý 1 triệu phần tử kết thúc trong nháy mắt.
Thực ra, nếu chỉ ở mức độ này thì VBA cũng có thể xử lý rất nhanh. Sự chênh lệch về hiệu suất sẽ hiện rõ khi có thêm các điều kiện rẽ nhánh (if/else) hoặc tính toán phức tạp ở giữa.
Trong thực tế công việc, hầu hết trường hợp chúng ta đều phải xử lý những logic phức tạp như vậy.
Cách truyền SafeArray 2 - Truyền nguyên cấu trúc SafeArray
Cách này phức tạp hơn Cách 1 một chút. VBA sẽ ném nguyên cục SafeArray sang, và phía Rust sẽ khôi phục và phân tích SafeArray đó.
Phương pháp này hữu ích trong các tình huống sau:
-
Khi muốn xử lý mảng 2 chiều (dữ liệu ma trận) Nếu dùng Cách 1 (chỉ truyền nội dung), bạn phải truyền thêm cả "số dòng" và "số cột" cùng với con trỏ, làm cho đối số trở nên rườm rà. Với SafeArray, vì
rgsabound(thông tin giới hạn) đã nằm trong cấu trúc, nên chỉ cần truyền 1 con trỏ là đủ. -
Khi kích thước mảng ở phía VBA có thể thay đổi Trong trường hợp "Dữ liệu có thể là 100 dòng, cũng có thể là 10 nghìn dòng", phía Rust chỉ cần đọc header của SafeArray là biết kích thước, giúp tiết kiệm công đoạn lấy
UBoundở phía VBA để truyền sang. -
Khi muốn Rust tính toán chỉ số (index) của mảng đa chiều một cách chính xác Mảng 2 chiều của VBA thực chất có thứ tự sắp xếp trên bộ nhớ ngược với ngôn ngữ C (ưu tiên cột - column major). Logic tính toán phức tạp này có thể được ẩn đi (encapsulate) ở phía Rust.
Bây giờ, tôi sẽ viết code để truyền mảng 2 chiều (kiểu Double) từ VBA, sau đó Rust sẽ phân tích cấu trúc SafeArray và trả về giá trị tổng.
Để tạo cấu trúc SafeArray trong Rust, bạn cần thêm crate windows. Hãy viết vào Cargo.toml như sau.
Phiên bản dưới đây là tại thời điểm tháng 12 năm 2025. Rust và các crate của Rust được cập nhật thường xuyên, nên hãy kiểm tra trên Docs.rs mỗi khi dùng.
[dependencies.windows]
version = "0.60"
features = ["Win32_System_Com"]
Nói thêm một chút, crate này wrap (bao bọc) Windows API nên có số lượng chức năng khổng lồ, việc tìm kiếm rất vất vả.
Trong trường hợp này, dùng AI tạo sinh (Generative AI) cũng được, nhưng vì Rust cập nhật quá nhanh nên thông tin từ AI thường không đáng tin, tự mình tìm kiếm nhiều khi còn chắc chắn và nhanh hơn.
Ví dụ lần này, cấu trúc SafeArray được định nghĩa ở trang sau (Cứ lên Docs.rs trước đã!).
Bạn sẽ thấy dòng windows::Win32::System::Com ở phía trên. Tài liệu C# cũng ghi rõ namespace kiểu như thế này nhỉ.
Biết được cái này, bạn sẽ biết cần bật flag nào trong Cargo.toml. Hãy thay tất cả dấu :: thành _ nhé!
Nó sẽ thành windows_Win32_System_Com. windows là tên crate nên chỉ lấy phần Win32_System_Com.
Sau đó ấn vào Feature flags ở trang đầu của crate windows.
Ở đây liệt kê rất nhiều flag. Hãy tìm Win32_System_Com. May mắn là chúng được sắp xếp theo thứ tự bảng chữ cái nên rất dễ tìm.
Thấy rồi! Có flag Win32_System_Com, vậy là đã chứng minh được chúng ta có thể dùng nó.
Biết cách tìm crate và flag như thế này thì sẽ không bị AI lừa nữa 😁
Các cấu trúc và hàm của COM thường sẽ dùng được bằng cách bật flag Win32_System_Com này.
use std::slice;
use windows::Win32::System::Com::SAFEARRAY;
#[unsafe(no_mangle)]
pub unsafe extern "system" fn sum_safe_array(psa: *mut SAFEARRAY) -> f64 {
unsafe {
// Kiểm tra Null
if psa.is_null() {
return 0.0;
}
// Kiểm tra số chiều (Kỳ vọng là mảng 2 chiều)
let dims = (*psa).cDims;
if dims != 2 {
// Xử lý lỗi (Lần này trả về 0 cho đơn giản)
return 0.0;
}
// Lấy thông tin chiều
// rgsabound[0] là thông tin "Cột" (Column), rgsabound[1] là "Dòng" (Row)
// ※ rgsabound của SafeArray được lưu theo thứ tự ngược lại với số chiều
let bounds_ptr = (*psa).rgsabound.as_ptr();
// Chuyển thành Slice của Rust để dễ xử lý
let bounds = slice::from_raw_parts(bounds_ptr, 2);
// Kích thước dòng và cột trong mảng 2 chiều của Excel
let cols = bounds[0].cElements as usize; // Số cột
let rows = bounds[1].cElements as usize; // Số dòng
// Truy cập vùng dữ liệu
let data_ptr = (*psa).pvData as *const f64;
let total_len = rows * cols;
// Tạo slice từ con trỏ thô (Đọc một lượt)
let data_slice = slice::from_raw_parts(data_ptr, total_len);
// Tính tổng
// ※ Ở đây chỉ là tính tổng đơn giản, nhưng khi truy cập bằng data_slice[index]
// cần chú ý rằng dữ liệu được ưu tiên theo Cột (Column-major).
// (Ví dụ: Thứ tự là A1, A2, A3 ... B1, B2, B3)
data_slice.iter().sum()
}
}
Phía VBA sẽ cài đặt như sau:
#If Win64 Then
' Khai báo hàm Rust (Đối số là Con trỏ đến SafeArray nên dùng LongPtr)
Private Declare PtrSafe Function sum_safe_array Lib "C:\my_rust_lib.dll" (ByVal psa As LongPtr) As Double
' Hàm lấy con trỏ mảng
Private Function GetSafeArrayPtr(ByRef arr() As Double) As LongPtr
' Not Not arr sẽ trả về địa chỉ (số) của cấu trúc SafeArray
GetSafeArrayPtr = Not Not arr
End Function
#End If
Sub TestSafeArrayPassing()
Dim arr() As Double
Dim i As Long, j As Long
' Chuẩn bị mảng 2 chiều (Ví dụ 100 dòng x 10 cột)
ReDim arr(1 To 100, 1 To 10)
' Nhập dữ liệu test
For i = 1 To 100
For j = 1 To 10
arr(i, j) = 1.0 ' Điền toàn bộ là 1
Next j
Next i
' Truyền sang Rust
' Truyền địa chỉ bắt đầu của cấu trúc SafeArray
Dim ptr As LongPtr
ptr = GetSafeArrayPtr(arr)
' Thực hiện tính toán
Dim result As Double
result = sum_safe_array(ptr)
' Xác nhận kết quả (100 * 10 * 1.0 = 1000 là đúng)
MsgBox "Sum Value: " & result
End Sub
Ở đây có 3 điểm cần lưu ý:
-
Lấy con trỏ
SafeArraycủa mảng động bằngNot Not arrDù là con trỏ của mảng động, nhưng nếu phía Rust cố nhận bằngLongPtrthì sẽ thất bại. Con trỏ đếnSafeArrayphải được lấy bằng một phương pháp đặc biệt. Đây là một dạng thủ thuật (trick), vì theo cách chính thống thì không thể lấy trực tiếp con trỏ từSafeArray. Tuy nhiên, nếu thêm toán tửNot, VBA sẽ không trả về bản thân mảng mà trả về giá trị đảo bit (bitwise NOT) của con trỏ. Tức là nếu0000_1111thì sẽ thành1111_0000. Nếu ta dùng thêm một lầnNotnữa để đảo ngược lại thì sẽ ra con trỏ gốc. Do đó, kết quả là nếu thêmNot2 lần, ta có thể lấy được con trỏ đếnSafeArray. Phương pháp này không làm được ở các ngôn ngữ khác, các ngôn ngữ đó đều có sẵn phương thức rõ ràng để lấy con trỏ. Tuy nhiên, nghe nói trongVisual Basicthời trước.NETthì hành vi cũng giống như vậy. Tóm lại, có vẻ như những đặc tả cũ của dòng ngôn ngữBASICvẫn chưa được cập nhật trong VBA. Không thể phủ nhận đây là một ngôn ngữ "di sản" (legacy). ※ Hãy chú ý là phương pháp này không dùng được cho mảng cố định (Fixed-size array). -
Thứ tự của
rgsaboundThông thường nếu định nghĩa trong VBA như dưới đây, nó có nghĩa là có 2 dòng và 5 cột.
' VBA: (Dòng, Cột)
Dim Arr(0 To 1, 0 To 4) As Long
Tuy nhiên, khi nhận cấu trúc SafeArray và nhìn vào nội dung rgsabound, thì Dòng và Cột lại nằm ngược nhau.
Tức là rgsabound[0] là số cột, và rgsabound[1] là số dòng. Điều này là do thứ tự sắp xếp trên bộ nhớ của VBA ngược với C.
Vì vậy, khi xử lý theo quy tắc quản lý bộ nhớ của C, ta cần nhận thức (0 To 1, 0 To 4) theo thứ tự từ phải sang trái.
Đây là tư tưởng thiết kế SafeArray của Microsoft nên chúng ta đành phải ép bản thân hiểu là "nó là như thế".
Nếu không hiểu điều này, ví dụ khi xử lý mảng hình vuông (2x2), bạn có thể sẽ không nhận ra lỗi.
- Số phần tử của
rgsaboundNếu nhìn vào định nghĩa tài liệu của crate,rgsaboundcó số phần tử là1. Tuy nhiên trong code, tôi đã dùnglet bounds = slice::from_raw_parts(bounds_ptr, 2);để lấy dữ liệu nhiều hơn số phần tử định nghĩa. Đây là đặc trưng "Flexible Array Member" (Thành viên mảng linh hoạt) của C, thực tế là có các dữ liệu tương ứng với số chiều thực tế nằm ẩn ngay sau số phần tử định nghĩa, nên đoạn code này lấy gộp tất cả chúng. Nếu bạn truy cập kiểubounds[1]một cách bình thường, nó sẽ mâu thuẫn với định nghĩa của crate và gây ra lỗi truy cập ngoài phạm vi (out of bounds), nhưng ở đây bằng cách lấy con trỏ và gom 2 dữ liệu nằm liền kề nhau, chúng ta có thể lấy được toàn bộ dữ liệu.
Chỉ riêng mảng kiểu số thôi đã phức tạp thế này rồi, nhưng mảng kiểu chuỗi (String) thì chúng ta sẽ phải xử lý con trỏ đến chuỗi chứ không phải bản thân giá trị. Nói cách khác, phía Rust sau khi nhận con trỏ mảng, sẽ phải tiếp tục khôi phục chuỗi ký tự từ các con trỏ chuỗi nằm bên trong đó.
Lần tới tôi sẽ giải thích về phương pháp này.
All rights reserved