+1

[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 SafeArray 1 - Chỉ truyền nội dung dữ liệu
  • Cách truyền SafeArray 2 - Truyền nguyên cấu trúc SafeArray

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Độ 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 đã!).

https://microsoft.github.io/windows-docs-rs/doc/windows/Win32/System/Com/struct.SAFEARRAY.html#method.clone

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ỏ SafeArray của mảng động bằng Not Not arr   Dù là con trỏ của mảng động, nhưng nếu phía Rust cố nhận bằng LongPtr thì sẽ thất bại. Con trỏ đến SafeArray phả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ếu 0000_1111 thì sẽ thành 1111_0000. Nếu ta dùng thêm một lần Not nữa để đảo ngược lại thì sẽ ra con trỏ gốc.   Do đó, kết quả là nếu thêm Not 2 lần, ta có thể lấy được con trỏ đến SafeArray.   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 trong Visual Basic thời trước .NET thì 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ữ BASIC vẫ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 rgsabound   Thô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.

<!-- end list -->
' 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 rgsabound   Nếu nhìn vào định nghĩa tài liệu của crate, rgsabound có số phần tử là 1. Tuy nhiên trong code, tôi đã dùng let 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ểu bounds[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

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í