Tạo, sử dụng thư viện liên kết động DLL (Standard DLL) trong VBA (1 người xem)

Người dùng đang xem chủ đề này

Tôi tuân thủ nội quy khi đăng bài

nguyendang95

Thành viên hoạt động
Tham gia
25/5/22
Bài viết
172
Được thích
161
Trong quá trình viết macro VBA trên Excel, chắc hẳn nhiều người dùng nhận ra nhiều hạn chế nhất định của ngôn ngữ lập trình này. DLL, hay thư viện liên kết động, có thể được viết bằng nhiều ngôn ngữ lập trình khác nhau (C/C++, Delphi, TwinBasic, v.v...) để VBA có thể khai báo và sử dụng thông qua từ khóa Declare statement (VBA).
Dưới đây là một ví dụ về việc viết và sử dụng DLL trong VBA, trong ví dụ này sử dụng code mẫu viết bằng TwinBasic và C/C++.
VD: Viết một DLL chứa hàm trả về mảng một chiều kiểu chuỗi gồm hai phần tử.
TwinBasic là một ngôn ngữ lập trình lấy cảm hứng từ VB6 và VB.NET nên cú pháp của nó khá tương đồng, giúp người dùng đã quen với VB có thể dễ dàng làm quen với ngôn ngữ lập trình này. Với thuận lợi này, người dùng có thể dễ dàng viết code một cách liền mạch, xuyên suốt.

1743477290646.png

Mã:
Module MainModule
    [DllExport]
    Public Function GetDemoArray() As Variant
        Dim arrResult(0 To 1) As String
        For i As Long = LBound(arrResult) To UBound(arrResult)
            arrResult(i) = "Đây là phần tử của mảng SAFEARRAY"
        Next
        Return arrResult
    End Function
End Module

Với C/C++, mọi thứ sẽ phức tạp hơn so với TwinBasic do cần phải khai báo và sử dụng nhiều hàm cần thiết để cho ra kết quả cuối cùng. Với VBA, kiểu String thực chất là kiểu BSTR, mảng là SAFEARRAY và Variant tương đương với VARIANT. Khi hàm trả về giá trị, người dùng không cần phải gọi hàm dọn dẹp (trường hợp ở đây là hàm VariantClear), đây là trách nhiệm của VBA sau khi sử dụng xong giá trị.

1743477808479.png

C++:
#include "pch.h"
#include "comutil.h"
#include "comdef.h"

extern "C" __declspec(dllexport) VARIANT WINAPI GetDemoArray() {
    VARIANT varResult;
    HRESULT hr;
    VariantInit(&varResult);
    SAFEARRAYBOUND sab[1]{};
    sab[0].cElements = 2;
    sab[0].lLbound = 0;
    SAFEARRAY* psa = SafeArrayCreate(VT_BSTR, 1, sab);
    if (!psa) {
        varResult.vt = VT_EMPTY;
        return varResult;
    }
    for (long i = 0; i < 2; i++) {
        BSTR bstrVal = SysAllocString(L"Đây là phẩn tử của mảng SAFEARRAY");
        if (!bstrVal) {
            varResult.vt = VT_EMPTY;
            SafeArrayDestroy(psa);
            return varResult;
        }
        hr = SafeArrayPutElement(psa, &i, (LPVOID)bstrVal);
        if (FAILED(hr)) {
            varResult.vt = VT_EMPTY;
            SafeArrayDestroy(psa);
            SysFreeString(bstrVal);
            return varResult;
        }
        SysFreeString(bstrVal);
    }
    varResult.vt = VT_ARRAY | VT_BSTR;
    varResult.parray = psa;
    return varResult;
}

VBA khai báo và sử dụng hàm trong DLL:
Để khai báo hàm DLL, ta sử dụng cấu trúc như sau:
Mã:
[ Public | Private ] Declare PtrSafe Function name Lib "đường_dẫn_dll"  [ ( [ tham_số_hàm_yêu_cầu] ) ] [ As type ]

1743478015362.png

Mã:
Option Explicit

Private Declare PtrSafe Function GetDemoArray Lib "Y:\data\CPlusPlus\SimpleDll\x64\Release\SimpleDll.dll" () As Variant
'Private Declare PtrSafe Function GetDemoArray Lib "Y:\data\twinBASIC_IDE_BETA\projects\Build\SimpleDll_win64.dll" () As Variant

Private Sub TestDll()
    Dim varResult As Variant
    varResult = GetDemoArray()
    Debug.Print varResult(1)
End Sub
 
Lần chỉnh sửa cuối:
TwinBasic có bản free và bản đó có tạo được DLL dạng này không bạn? Nếu có thì dùng nó cho vba hợp với mọi người chỉ biết vba.
 
TwinBasic có bản free và bản đó có tạo được DLL dạng này không bạn? Nếu có thì dùng nó cho vba hợp với mọi người chỉ biết vba.
Tạo được nhé, hạn chế là vẫn đang ở giai đoạn beta, và khi biên dịch ra DLL dùng cho Office 64bit thì sẽ hiện ra ảnh banner tầm 5 giây khá khó chịu.
Bạn tải ở đây: twinBASIC BETA .
 
Tạo được nhé, hạn chế là vẫn đang ở giai đoạn beta, và khi biên dịch ra DLL dùng cho Office 64bit thì sẽ hiện ra ảnh banner tầm 5 giây khá khó chịu.
Bạn tải ở đây: twinBASIC BETA .

Hay quá. Tui có vài dự án chết ngỏm vì VB6 không hỗ trợ 64 bit, nếu thằng này làm tốt cho x64 thì mua cũng đáng.
 
Làm thế nào mà lại hướng dẫn tạo tham chiếu đường dẫn tuyệt đối cho API kiểu này được bạn.
"Y:\data\CPlusPlus\SimpleDll\x64\Release\SimpleDll.dll"
Rồi người ta tải về sử dụng bằng cách nào để có được đường dẫn này. Cách lập trình này chỉ dành cho chính lập trình viên sử dụng mà thôi.

Nên hướng dẫn theo hướng sử dụng các API là LoadLibrary và DispCallFunc.
Hướng dẫn sai lầm dễ làm con người ta u mê mãi không thoát ra được cái vòng luẩn quẩn của lập trình.
 
Làm thế nào mà lại hướng dẫn tạo tham chiếu đường dẫn tuyệt đối cho API kiểu này được bạn.
"Y:\data\CPlusPlus\SimpleDll\x64\Release\SimpleDll.dll"
Rồi người ta tải về sử dụng bằng cách nào để có được đường dẫn này. Cách lập trình này chỉ dành cho chính lập trình viên sử dụng mà thôi.

Nên hướng dẫn theo hướng sử dụng các API là LoadLibrary và DispCallFunc.
Hướng dẫn sai lầm dễ làm con người ta u mê mãi không thoát ra được cái vòng luẩn quẩn của lập trình.
Vậy bạn hãy trình bày thêm ở đây cho mọi người cùng biết, bài viết này chỉ hướng đến người đã hiểu rõ dll là gì và làm cách nào sử dụng nó mà thôi.
 
Vấn đề này căng, kiến thức lập trình không đủ, khó đi theo hướng này. Để hướng dẫn đầy đủ chắc mất nhiều trang bài viết. Rồi phải nhồi nhét kiến thức chuyên sâu về lập trình API. Chắc phải tự nỗ lực học tập từ từ thôi. Chứ đến đây, nó là cấp độ lập trình khác hoàn toàn so với những cơ bản của VBA. Dễ thì ai cũng làm được rồi.
Chưa kể phải biết xử lý mã để trình quét virus xem là an toàn.

Thật ra vấn đề này không biết được bao nhiêu người đủ khả năng tiếp thu, chắc là 1 hoặc 2 và 3.

Tôi chỉ có thể đề xuất các bước học được kiến thức này:
1. Thông thuộc các API xử lý bộ nhớ, xử lý chuỗi, xử lý mảng, ...
2. Hiểu về giao diện lớp như: QueryInterface, AddRef, Release, GetTypeInfoCount, GetTypeInfo, GetIDsOfNames, Invoke
3. Hiểu về con trỏ bộ nhớ.
4. Hiểu về kiến trúc 32 và 64 bit cho lập trình API.
5. Sử dụng các ứng dụng bổ trợ như COMView, TypeLib Browser để xem thông tin của một DLL.
 
Mình nghĩ trên đây chỉ hướng dẫn cơ bản thôi chứ còn nâng cao thì vô bờ bến và dành cho người chuyên nghiệp hoặc chủ để nâng cao khác.
 
Thư viện dll này có cần phải đăng ký với hệ thống không ? hay chỉ cần trỏ tới là được vậy bạn @nguyendang95 ?
 
Thư viện dll này có cần phải đăng ký với hệ thống không ? hay chỉ cần trỏ tới là được vậy bạn @nguyendang95 ?
Không cần đăng ký nhé, chỉ OCX và ActiveX DLL mới cần đăng ký, trình đăng ký như regsvr32 và regasm sẽ gọi hàm DllRegisterServer có trong OCX và ActiveX DLL để ghi thông tin cần thiết vào registry của hệ thống.
Bài đã được tự động gộp:

Vấn đề này căng, kiến thức lập trình không đủ, khó đi theo hướng này. Để hướng dẫn đầy đủ chắc mất nhiều trang bài viết. Rồi phải nhồi nhét kiến thức chuyên sâu về lập trình API. Chắc phải tự nỗ lực học tập từ từ thôi. Chứ đến đây, nó là cấp độ lập trình khác hoàn toàn so với những cơ bản của VBA. Dễ thì ai cũng làm được rồi.
Chưa kể phải biết xử lý mã để trình quét virus xem là an toàn.

Thật ra vấn đề này không biết được bao nhiêu người đủ khả năng tiếp thu, chắc là 1 hoặc 2 và 3.

Tôi chỉ có thể đề xuất các bước học được kiến thức này:
1. Thông thuộc các API xử lý bộ nhớ, xử lý chuỗi, xử lý mảng, ...
2. Hiểu về giao diện lớp như: QueryInterface, AddRef, Release, GetTypeInfoCount, GetTypeInfo, GetIDsOfNames, Invoke
3. Hiểu về con trỏ bộ nhớ.
4. Hiểu về kiến trúc 32 và 64 bit cho lập trình API.
5. Sử dụng các ứng dụng bổ trợ như COMView, TypeLib Browser để xem thông tin của một DLL.
1 và 3: Cứ C/C++ mà học thôi.
2. Cái này liên quan đến COM rồi, nếu cần tạo ActiveX DLL thì cần nắm vững kiến thức về COM chứ bài viết này chỉ bàn về Standard DLL.
Nói chung là nắm được cách tạo standard dll thì có thể làm được nhiều thứ, một số phần mềm cho phép bổ sung tính năng thông qua plugin hoặc add-in dưới dạng dll như thế này, ví dụ như add-in xll của Excel và data plugin dùng cho Amibroker mà tôi hay viết cho một số người chơi chứng khoán và tiền mã hóa.
 
Lần chỉnh sửa cuối:
Tôi chỉ đề xuất các hướng để đạt được cách gọi hàm DLL từ VBA, không phải COM gì ở đây.

Bạn sang đây xem một cách triển khai Standard DLL, mà không phụ thuộc tham chiếu API
https://www.giaiphapexcel.com/diendan/threads/166377/#post-1140435

Tôi không thích Amibroker cho lắm, giao diện tệ hại, và nó là có phí. Thời hiện đại rồi, viết tận dụng giao diện Web hiện đại hoặc WebView2, hoặc ElectronJs mà viết.
TradingView là một ví dụ điển hình.
 
Mã:
Module MainModule
    [DllExport]
    Public Function GetDemoArray() As Variant
        Dim arrResult(0 To 1) As String
        For i As Long = LBound(arrResult) To UBound(arrResult)
            arrResult(i) = "Đây là phần tử của mảng SAFEARRAY"
        Next
        Return arrResult
    End Function
End Module

Với C/C++, mọi thứ sẽ phức tạp hơn so với TwinBasic do cần phải khai báo và sử dụng nhiều hàm cần thiết để cho ra kết quả cuối cùng. Với VBA, kiểu String thực chất là kiểu BSTR, mảng là SAFEARRAY và Variant tương đương với VARIANT. Khi hàm trả về giá trị, người dùng không cần phải gọi hàm dọn dẹp (trường hợp ở đây là hàm VariantClear), đây là trách nhiệm của VBA sau khi sử dụng xong giá trị.

View attachment 307699

Cách tạo mảng SAFEARRAY trong Delphi có thể làm giống như C/C++ và cũng có cách để Delphi tự làm như sau
//----------------------------------------------------
function GetDemoArray1D(): OleVariant; stdcall
var
n: Integer;
arr: array of string;
begin
SetLength(arr, 2);
for n := 0 to 1 do
arr[n] := 'Đây là phần tử thứ ' + n.ToString;
Result := arr;// Delphi auto convert dynamic array 1D to safearray.
end;
//----------------------------------------------------
exports
GetDemoArray1D;
//----------------------------------------------------


Delphi có hai cách lập trình vừa cho phép người lập trình tự tay viết từng lệnh xử lý chi tiết bên trong giống C/C++ và cũng vừa cho phép người lập trình làm hết sức đơn giản và để Delphi tự làm giúp các kỹ thuật cần thiết đằng sau đó, việc này giống như VB/VBA. Nên Delphi và VBA rất giống nhau nếu lập trình ở mức cơ bản, còn lập trình nâng cao thì Delphi cũng có các kỹ thuật khá giống với C/C++. Nếu ai muốn thêm một lựa chọn ngôn ngữ lập trình Delphi để tạo Standard DLL hay các kiểu thì xem chủ đề này mình chia sẻ trên GPE lâu rồi.

Nếu ai không muốn học thêm một ngôn ngữ mới như C/C++, Delphi để tạo DLL hoặc không muốn lập trình can thiệp sâu vào hệ thống thì lựa chọn TwinBasic có lẽ là phù hợp nhất hiện nay.

TwinBasic hình như chưa có Remote Debug và chưa Debug DLL trên Host App như Delphi và các IDE của .NET thì phải.
 
Cách tạo mảng SAFEARRAY trong Delphi có thể làm giống như C/C++ và cũng có cách để Delphi tự làm như sau
//----------------------------------------------------
function GetDemoArray1D(): OleVariant; stdcall
var
n: Integer;
arr: array of string;
begin
SetLength(arr, 2);
for n := 0 to 1 do
arr[n] := 'Đây là phần tử thứ ' + n.ToString;
Result := arr;// Delphi auto convert dynamic array 1D to safearray.
end;
//----------------------------------------------------
exports
GetDemoArray1D;
//----------------------------------------------------


Delphi có hai cách lập trình vừa cho phép người lập trình tự tay viết từng lệnh xử lý chi tiết bên trong giống C/C++ và cũng vừa cho phép người lập trình làm hết sức đơn giản và để Delphi tự làm giúp các kỹ thuật cần thiết đằng sau đó, việc này giống như VB/VBA. Nên Delphi và VBA rất giống nhau nếu lập trình ở mức cơ bản, còn lập trình nâng cao thì Delphi cũng có các kỹ thuật khá giống với C/C++. Nếu ai muốn thêm một lựa chọn ngôn ngữ lập trình Delphi để tạo Standard DLL hay các kiểu thì xem chủ đề này mình chia sẻ trên GPE lâu rồi.

Nếu ai không muốn học thêm một ngôn ngữ mới như C/C++, Delphi để tạo DLL hoặc không muốn lập trình can thiệp sâu vào hệ thống thì lựa chọn TwinBasic có lẽ là phù hợp nhất hiện nay.

TwinBasic hình như chưa có Remote Debug và chưa Debug DLL trên Host App như Delphi và các IDE của .NET thì phải.
Trong Delphi chắc là gói gọn các hàm trong comdef.h, comutil.h sao cho tương thích với kiểu dữ liệu của Delphi hơn.
Nói về cách tạo mảng SAFEARRAY, sử dụng mấy hàm C như SafeArrayCreate, SafeArrayGetElement, SafeArrayAccessData, SafeArrayDestroy, v.v., khá rườm rà, khiến cho code trở nên dài dòng và phức tạp nên mấy năm trước Microsoft tạo ra lớp ATL gói gọn là Simplify Safe Array Programming in C++ with CComSafeArray để dùng cho tiện.
TwinBasic vẫn đang ở dạng beta thôi, giờ đang lo sửa mấy cái lỗi bug lặt vặt liên quan đến form chưa xong, làm xong mấy tính năng debug vẫn còn xa lắm.
Bài đã được tự động gộp:

Tôi chỉ đề xuất các hướng để đạt được cách gọi hàm DLL từ VBA, không phải COM gì ở đây.

Bạn sang đây xem một cách triển khai Standard DLL, mà không phụ thuộc tham chiếu API
https://www.giaiphapexcel.com/diendan/threads/166377/#post-1140435

Tôi không thích Amibroker cho lắm, giao diện tệ hại, và nó là có phí. Thời hiện đại rồi, viết tận dụng giao diện Web hiện đại hoặc WebView2, hoặc ElectronJs mà viết.
TradingView là một ví dụ điển hình.
Tôi thấy Amibroker cũng ok mà, giao diện tuy hơi lạc hậu so với thời đại hiện nay, nhưng hiệu năng chạy rất tốt, tôi từng viết data plugin lấy dữ liệu thời gian thực bằng websocket và Amibroker chạy cực kỳ mượt, treo 50 mã vẫn mượt, dung lượng RAM tiêu tốn cũng ít.
 
Trong Delphi chắc là gói gọn các hàm trong comdef.h, comutil.h sao cho tương thích với kiểu dữ liệu của Delphi hơn.
Nói về cách tạo mảng SAFEARRAY, sử dụng mấy hàm C như SafeArrayCreate, SafeArrayGetElement, SafeArrayAccessData, SafeArrayDestroy, v.v., khá rườm rà, khiến cho code trở nên dài dòng và phức tạp nên mấy năm trước Microsoft tạo ra lớp ATL gói gọn là Simplify Safe Array Programming in C++ with CComSafeArray để dùng cho tiện.

Delphi có các thư viện rất mạnh họ đã viết lại và wrap đến các hàm API của Windows, ví dụ làm việc với kiểu tagVARIANT (đây là kiểu dữ liệu phổ biết cho lập trình COM/OLE mà lập trình VBA chúng ta hay đụng đến như kiểu Variant) họ tạo ra kiểu Variant, OleVariant giao tiếp với các hàm API và thực hiện gán và nhận dễ dàng giống như trong VB/VBA. Các hàm SafeArrayCreate, SafeArrayGetElement, SafeArrayAccessData, SafeArrayDestroy,... có nhiệm vụ để làm việc với mảng SAFEARRAY mà Windows quy định, chúng nằm trong DLL OleAut32.dll, các ngôn ngữ lập trình như C/C++, Delphi, VB/VBA, C#,... chỉ là wrap đến chúng mà thôi. Bản chất các trình biên dịch chỉ hỗ trợ các tiện ích, thư viện để đơn giản hóa còn bản chất đằng sau đó chúng đều làm những gì mà Windows quy định. Ở level cao, trong hoàn cảnh nào đó vẫn phải moi móc thủ công với các hàm API dù rườm rà, nếu cứ dùng những thứ tự động không phải lúc nào cũng đạt được đúng mục đích.
 
Lần chỉnh sửa cuối:
Với C/C++, mọi thứ sẽ phức tạp hơn so với TwinBasic do cần phải khai báo và sử dụng nhiều hàm cần thiết để cho ra kết quả cuối cùng. Với VBA, kiểu String thực chất là kiểu BSTR, mảng là SAFEARRAY và Variant tương đương với VARIANT. Khi hàm trả về giá trị, người dùng không cần phải gọi hàm dọn dẹp (trường hợp ở đây là hàm VariantClear), đây là trách nhiệm của VBA sau khi sử dụng xong giá trị.

C++:
#include "pch.h"
#include "comutil.h"
#include "comdef.h"

extern "C" __declspec(dllexport) VARIANT WINAPI GetDemoArray() {
    VARIANT varResult;
    HRESULT hr;
    VariantInit(&varResult);
    SAFEARRAYBOUND sab[1]{};
    sab[0].cElements = 2;
    sab[0].lLbound = 0;
    SAFEARRAY* psa = SafeArrayCreate(VT_BSTR, 1, sab);
    if (!psa) {
        varResult.vt = VT_EMPTY;
        return varResult;
    }
    for (long i = 0; i < 2; i++) {
        BSTR bstrVal = SysAllocString(L"Đây là phẩn tử của mảng SAFEARRAY");
        if (!bstrVal) {
            varResult.vt = VT_EMPTY;
            SafeArrayDestroy(psa);
            return varResult;
        }
        hr = SafeArrayPutElement(psa, &i, (LPVOID)bstrVal);
        if (FAILED(hr)) {
            varResult.vt = VT_EMPTY;
            SafeArrayDestroy(psa);
            SysFreeString(bstrVal);
            return varResult;
        }
        SysFreeString(bstrVal); //<== Lỗi ở đây
    }
    varResult.vt = VT_ARRAY | VT_BSTR;
    varResult.parray = psa;
    return varResult;
}

Tôi xem lại ví dụ viết bằng C của bạn có một lỗi nghiêm trọng đó là dòng lệnh giải phóng bộ nhớ SysFreeString(bstrVal); sau khi đã SafeArrayPutElement thành công. Việc này là đã tự xóa vùng nhớ của con trỏ BSTR trong phần tử mảng đó. Chạy trên một hoàn cảnh nào đó có thể không thấy lỗi nhưng nó luôn tiềm ẩn lỗi bất cứ lúc nào trong quá trình cấp phát vùng nhớ để lưu chuỗi, dấu hiệu là giá trị lấy ra từ mảng là dữ liệu trắng hoặc rác. Lỗi này trình biên dịch không phát hiện được mà nó nằm ở cấp phát bộ nhớ lưu dữ liệu.

Thực tế tạo hàm trong DLL để export dùng bên ngoài không hề dễ nếu liên quan đến mảng và string. Đặc biệt với C/C++ người lập trình phải tự làm rất nhiều, vì phải tự làm nên nếu không cẩn thận nhầm lẫn hoặc không đúng nguyên lý thì ứng dụng sẽ tai nạn bất cứ lúc nào.
 
Lần chỉnh sửa cuối:
Tôi xem lại ví dụ viết bằng C của bạn có một lỗi nghiêm trọng đó là dòng lệnh giải phóng bộ nhớ SysFreeString(bstrVal); sau khi đã SafeArrayPutElement thành công. Việc này là đã tự xóa vùng nhớ của con trỏ BSTR trong phần tử mảng đó. Chạy trên một hoàn cảnh nào đó có thể không thấy lỗi nhưng nó luôn tiềm ẩn lỗi bất cứ lúc nào trong quá trình cấp phát vùng nhớ để lưu chuỗi, dấu hiệu là giá trị lấy ra từ mảng là dữ liệu trắng hoặc rác. Lỗi này trình biên dịch không phát hiện được mà nó nằm ở cấp phát bộ nhớ lưu dữ liệu.

Thực tế tạo hàm trong DLL để export dùng bên ngoài không hề dễ nếu liên quan đến mảng và string. Đặc biệt với C/C++ người lập trình phải tự làm rất nhiều, vì phải tự làm nên nếu không cẩn thận nhầm lẫn hoặc không đúng nguyên lý thì ứng dụng sẽ tai nạn bất cứ lúc nào.
Vậy tại sao macro VBA trong bài viết vẫn cho ra kết quả bình thường mà đáng lẽ theo suy luận của bạn thì chương trình phải bị crash ngay khi VBA truy cập phần tử bất kỳ của mảng, hoặc khi gỡ lỗi (debug) bằng trình soạn thảo code thì hiện thông báo lỗi access violation (đại loại là truy cập đến địa chỉ vùng nhớ không tồn tại)? Câu trả lời đơn giản thôi, hàm SafeArrayPutElement chỉ sao chép giá trị mà con trỏ trỏ đến thôi. Bạn xem phần Lưu ý (Remarks) dưới đây thì sẽ hiểu. Hàm sau khi được gọi dù thành công (SUCCEEDED(hr) == TRUE) hay không thì phải giải phóng bộ nhớ đã cấp phát cho biến bstrVal, nếu không chương trình sẽ bị rò rỉ bộ nhớ (memory leak) theo thời gian.
SafeArrayPutElement function (oleauto.h)
Allocating and Releasing Memory for a BSTR
1743584627278.png
Tham khảo thêm một số câu trả lời trên Stack Overflow:
COM: Create a VT_ARRAY with VT_BSTR values
Regarding SafeArrayPutElement
 
Lần chỉnh sửa cuối:
Vậy tại sao macro VBA trong bài viết vẫn cho ra kết quả bình thường mà đáng lẽ theo suy luận của bạn thì chương trình phải bị crash ngay khi VBA truy cập phần tử bất kỳ của mảng, hoặc khi gỡ lỗi (debug) bằng trình soạn thảo code thì hiện thông báo lỗi access violation (đại loại là truy cập đến địa chỉ vùng nhớ không tồn tại)? Câu trả lời đơn giản thôi, hàm SafeArrayPutElement chỉ sao chép giá trị mà con trỏ trỏ đến thôi. Bạn xem phần Lưu ý (Remarks) dưới đây thì sẽ hiểu. Hàm sau khi được gọi thành công (SUCCEEDED(hr) == TRUE) thì phải giải phóng bộ nhớ đã cấp phát cho biến bstrVal, nếu không chương trình sẽ bị rò rỉ bộ nhớ (memory leak) theo thời gian.
SafeArrayPutElement function (oleauto.h)
Allocating and Releasing Memory for a BSTR
View attachment 307723
Tham khảo thêm một số câu trả lời trên Stack Overflow:
COM: Create a VT_ARRAY with VT_BSTR values
Regarding SafeArrayPutElement

Tôi từng gặp lỗi này và khi không dùng SysFreeString thì không bị nữa. Loại lỗi này như tôi nói nó biểu hiện là mất dữ liệu hoặc dữ liệu rác chứ không gây lỗi kiểu mất địa chỉ truy xuất dữ liệu như "access violation". Còn nếu hàm SafeArrayPutElement copy BSTR thì đương nhiên không có lỗi và sẽ phải Free nó sau khi copy thành công. MS không nói rõ lắm cơ chế Put data của hàm này với loại string.
 
Tôi từng gặp lỗi này và khi không dùng SysFreeString thì không bị nữa. Loại lỗi này như tôi nói nó biểu hiện là mất dữ liệu hoặc dữ liệu rác chứ không gây lỗi kiểu mất địa chỉ truy xuất dữ liệu như "access violation". Còn nếu hàm SafeArrayPutElement copy BSTR thì đương nhiên không có lỗi và sẽ phải Free nó sau khi copy thành công. MS không nói rõ lắm cơ chế Put data của hàm này với loại string.
Vấn đề này theo tôi nghĩ phải xem kỹ lại tài liệu của các hàm liên quan đến kiểu BSTR trong Delphi mới rõ được. Trong C/C++ với kiểu BSTR và VARIANT, lập trình viên có thể tự động cấp phát và giải phóng bộ nhớ bằng cách sử dụng lớp gói gọn của hai kiểu dữ liệu này mà Microsoft cung cấp lần lượt là _bstr_t class_variant_t Class, ngoài ra chúng còn cung cấp một số phương thức giúp tương tác với hai kiểu dữ liệu này vừa chính xác lại vừa an toàn, ví dụ như có thể dễ dàng ghép hai chuỗi (concatenate) BSTR bằng cách gói gọn chúng trong lớp gói gọn _bstr_t và dùng toán tử "+"giữa hai lớp gói gọn là xong, không cần phải phiền phức gọi SysAllocString, SysAllocStringLen, SysAllocStringByteLen, SysAllocByteString, SysStringLen, SysStringByteLen rồi lại memcpy vừa phiền phức lại vừa dễ mắc sai lầm.
 
Lần chỉnh sửa cuối:
Vấn đề này theo tôi nghĩ phải xem kỹ lại tài liệu của các hàm liên quan đến kiểu BSTR trong Delphi mới rõ được. Trong C/C++ với kiểu BSTR và VARIANT, lập trình viên có thể tự động cấp phát và giải phóng bộ nhớ bằng cách sử dụng lớp gói gọn của hai kiểu dữ liệu này mà Microsoft cung cấp lần lượt là _bstr_t class_variant_t Class, ngoài ra chúng còn cung cấp một số phương thức giúp tương tác với hai kiểu dữ liệu này vừa chính xác lại vừa an toàn, ví dụ như có thể dễ dàng ghép hai chuỗi (concatenate) BSTR bằng cách gói gọn chúng trong lớp gói gọn _bstr_t và dùng toán tử "+"giữa hai lớp gói gọn là xong, không cần phải phiền phức gọi SysAllocString, SysAllocStringLen, SysAllocStringByteLen, SysAllocByteString, SysStringLen, SysStringByteLen rồi lại memcpy vừa phiền phức lại vừa dễ mắc sai lầm.

Về String và Variant thì Delphi làm rất tốt, tiện dụng y như lập trình trong VB/VBA là bạn hiểu. Mọi thứ cấp phát và giải phóng bộ nhớ liên quan đến Variant, String Delphi tự làm cho hết (kiểu con trỏ thì khác). Chỉ khi muốn tự làm thì mới phải làm giống C/C++ thôi.
 
Đã lập trình C/C++ thì tận dụng các hàm xử lý bộ nhớ và kernel mạnh mẽ nhất, để đạt được hiệu suất, tối ưu, hiệu quả quản lý bộ nhớ. Toán tử + buộc cấp phát bộ nhớ mới, đường nào mà nó không gọi đến các api SysAllocString,... khi xử lý chuỗi, nó chỉ nhanh khi + 1, 2 lần. memcpy nó là một phần của lập trình cấp thấp, nên lệnh này rất quan trọng, không thể bỏ qua được.
 
Trong quá trình viết macro VBA trên Excel, chắc hẳn nhiều người dùng nhận ra nhiều hạn chế nhất định của ngôn ngữ lập trình này. DLL, hay thư viện liên kết động, có thể được viết bằng nhiều ngôn ngữ lập trình khác nhau (C/C++, Delphi, TwinBasic, v.v...) để VBA có thể khai báo và sử dụng thông qua từ khóa Declare statement (VBA).
Dưới đây là một ví dụ về việc viết và sử dụng DLL trong VBA, trong ví dụ này sử dụng code mẫu viết bằng TwinBasic và C/C++.
VD: Viết một DLL chứa hàm trả về mảng một chiều kiểu chuỗi gồm hai phần tử.
TwinBasic là một ngôn ngữ lập trình lấy cảm hứng từ VB6 và VB.NET nên cú pháp của nó khá tương đồng, giúp người dùng đã quen với VB có thể dễ dàng làm quen với ngôn ngữ lập trình này. Với thuận lợi này, người dùng có thể dễ dàng viết code một cách liền mạch, xuyên suốt.

View attachment 307698

Mã:
Module MainModule
    [DllExport]
    Public Function GetDemoArray() As Variant
        Dim arrResult(0 To 1) As String
        For i As Long = LBound(arrResult) To UBound(arrResult)
            arrResult(i) = "Đây là phần tử của mảng SAFEARRAY"
        Next
        Return arrResult
    End Function
End Module

Với C/C++, mọi thứ sẽ phức tạp hơn so với TwinBasic do cần phải khai báo và sử dụng nhiều hàm cần thiết để cho ra kết quả cuối cùng. Với VBA, kiểu String thực chất là kiểu BSTR, mảng là SAFEARRAY và Variant tương đương với VARIANT. Khi hàm trả về giá trị, người dùng không cần phải gọi hàm dọn dẹp (trường hợp ở đây là hàm VariantClear), đây là trách nhiệm của VBA sau khi sử dụng xong giá trị.

View attachment 307699

C++:
#include "pch.h"
#include "comutil.h"
#include "comdef.h"

extern "C" __declspec(dllexport) VARIANT WINAPI GetDemoArray() {
    VARIANT varResult;
    HRESULT hr;
    VariantInit(&varResult);
    SAFEARRAYBOUND sab[1]{};
    sab[0].cElements = 2;
    sab[0].lLbound = 0;
    SAFEARRAY* psa = SafeArrayCreate(VT_BSTR, 1, sab);
    if (!psa) {
        varResult.vt = VT_EMPTY;
        return varResult;
    }
    for (long i = 0; i < 2; i++) {
        BSTR bstrVal = SysAllocString(L"Đây là phẩn tử của mảng SAFEARRAY");
        if (!bstrVal) {
            varResult.vt = VT_EMPTY;
            SafeArrayDestroy(psa);
            return varResult;
        }
        hr = SafeArrayPutElement(psa, &i, (LPVOID)bstrVal);
        if (FAILED(hr)) {
            varResult.vt = VT_EMPTY;
            SafeArrayDestroy(psa);
            SysFreeString(bstrVal);
            return varResult;
        }
        SysFreeString(bstrVal);
    }
    varResult.vt = VT_ARRAY | VT_BSTR;
    varResult.parray = psa;
    return varResult;
}

VBA khai báo và sử dụng hàm trong DLL:
Để khai báo hàm DLL, ta sử dụng cấu trúc như sau:
Mã:
[ Public | Private ] Declare PtrSafe Function name Lib "đường_dẫn_dll"  [ ( [ tham_số_hàm_yêu_cầu] ) ] [ As type ]

View attachment 307700

Mã:
Option Explicit

Private Declare PtrSafe Function GetDemoArray Lib "Y:\data\CPlusPlus\SimpleDll\x64\Release\SimpleDll.dll" () As Variant
'Private Declare PtrSafe Function GetDemoArray Lib "Y:\data\twinBASIC_IDE_BETA\projects\Build\SimpleDll_win64.dll" () As Variant

Private Sub TestDll()
    Dim varResult As Variant
    varResult = GetDemoArray()
    Debug.Print varResult(1)
End Sub
mạo muội xin các bác chỉ giáo rằng việc tạo .dll như kia tác dụng là gì trong khi mình có thể viết thẳng function trong file excel ạ !?
 
Đã lập trình C/C++ thì tận dụng các hàm xử lý bộ nhớ và kernel mạnh mẽ nhất, để đạt được hiệu suất, tối ưu, hiệu quả quản lý bộ nhớ. Toán tử + buộc cấp phát bộ nhớ mới, đường nào mà nó không gọi đến các api SysAllocString,... khi xử lý chuỗi, nó chỉ nhanh khi + 1, 2 lần. memcpy nó là một phần của lập trình cấp thấp, nên lệnh này rất quan trọng, không thể bỏ qua được.
Trong VBA nếu bạn cần sử dụng bộ nhớ cho một số mục đích nào đó, vd: nối hàng chục chuỗi lại với nhau kiểu StringBuider, thì có thể sử dụng hàm CoTaskMemAlloc function (combaseapi.h) để xin cấp phát bộ nhớ động vùng heap.

1743647722909.png

Tham khảo thêm: Comparing Memory Allocation Methods
Bài đã được tự động gộp:

mạo muội xin các bác chỉ giáo rằng việc tạo .dll như kia tác dụng là gì trong khi mình có thể viết thẳng function trong file excel ạ !?
Dùng DLL khi VBA thiếu một số chức năng mà bản thân nó không thể tự làm được. VD: Bạn có thể viết một hàm tính toán đa luồng, biên dịch thành DLL rồi sử dụng nó trong VBA nhằm giải quyết vấn đề VBA không hỗ trợ đa luồng.
 
Nối chuỗi hiệu suất phải sử dụng xử lý mảng byte, con trỏ bộ nhớ, cấp phát bộ nhớ.

Đoạn mã bạn đề xuất không phải nối chuỗi mà là đặt chuỗi vào bên trái của bộ nhớ đã cấp phát.
Và đoạn mã này viết lỗi và không tương thích 32 và 64 bit.
 
Nối chuỗi hiệu suất phải sử dụng xử lý mảng byte, con trỏ bộ nhớ, cấp phát bộ nhớ.

Đoạn mã bạn đề xuất không phải nối chuỗi mà là đặt chuỗi vào bên trái của bộ nhớ đã cấp phát.
Và đoạn mã này viết lỗi và không tương thích 32 và 64 bit.
Vậy bạn đề xuất xem nên sửa lại thế nào để nó có thể chạy trên cả hai bản 32 bit và 64bit, chỉ tính từ bản Office 2010 trở đi?
 
Mã ở trên không cần thiết trong VBA, mặc dù sửa lại rất đơn giản.

Đây là một lớp StringBuilder cho VB, có thể cấp phát mới, có thể clone lại một StringBuilder, gộp, tái tạo


JavaScript:
Option Explicit
#If VBA7 Then
  Private Declare PtrSafe Sub CopyMemory Lib "kernel32.dll" Alias "RtlMoveMemory" (ByVal dst As LongPtr, ByVal src As LongPtr, ByVal Length As LongPtr)
#Else
  Private Declare Sub CopyMemory Lib "kernel32.dll" Alias "RtlMoveMemory" (ByVal dst As Long, ByVal src As Long, ByVal Length As Long)
#End If
Private clb&, cBuffer() As Byte, l&, lb&, ns&, U&, b() As Byte
Private Sub Class_Initialize()
  Create
End Sub
Public Sub Create(Optional ByVal Content As String, Optional ByVal initialCapacity As Long = 255)
  l = Len(Content): cBuffer = Content: clb = l * 2
  If initialCapacity < l Then U = clb - 1 Else U = initialCapacity * 2 - 1: ReDim Preserve cBuffer(0 To U)
End Sub
Public Sub Append(ByVal value As String)
  lb = LenB(value): If lb = 0 Then Exit Sub
  ns = clb + lb
  If ns > (U + 1) Then U = ns - 1: ReDim Preserve cBuffer(U)
  CopyMemory VarPtr(cBuffer(clb)), StrPtr(value), lb: clb = ns
End Sub

Public Sub join(ByVal Builder As StringBuilder)
  With Builder
    lb = .LenBuffer: If lb = 0 Then Exit Sub
    ns = clb + lb
    If ns > (U + 1) Then U = ns - 1: ReDim Preserve cBuffer(U)
    b = .buffer()
    CopyMemory VarPtr(cBuffer(clb)), VarPtr(b(0)), lb: clb = ns
  End With
End Sub
Public Sub clone(ByVal Builder As StringBuilder)
  With Builder
    clb = .LenBuffer: U = .Capacity: cBuffer = .buffer
  End With
End Sub
Public Property Get buffer(Optional fixLength As Boolean) As Byte()
  If fixLength And clb > 0 Then
    b = cBuffer: ReDim Preserve b(clb - 1): buffer = b
  Else
    buffer = cBuffer:
  End If
End Property
Public Property Get Length() As Long: Length = clb / 2: End Property
Public Property Get LenBuffer() As Long: LenBuffer = clb: End Property
Public Property Get Capacity() As Long: Capacity = U: End Property

Public Sub getString(refString$)
  If clb > 0 Then b = cBuffer: ReDim Preserve b(clb - 1): refString = b Else refString = vbNullString
End Sub
Public Function ToString() As String
'add:Attribute ToString.VB_UserMemId = 0
  If clb > 0 Then b = cBuffer: ReDim Preserve b(clb - 1): ToString = b
End Function
Sub reset()
  clb = 0: ReDim cBuffer(0 To U)
End Sub
 
Mã ở trên không cần thiết trong VBA, mặc dù sửa lại rất đơn giản.

Đây là một lớp StringBuilder cho VB, có thể cấp phát mới, có thể clone lại một StringBuilder, gộp, tái tạo


JavaScript:
Option Explicit
#If VBA7 Then
  Private Declare PtrSafe Sub CopyMemory Lib "kernel32.dll" Alias "RtlMoveMemory" (ByVal dst As LongPtr, ByVal src As LongPtr, ByVal Length As LongPtr)
#Else
  Private Declare Sub CopyMemory Lib "kernel32.dll" Alias "RtlMoveMemory" (ByVal dst As Long, ByVal src As Long, ByVal Length As Long)
#End If
Private clb&, cBuffer() As Byte, l&, lb&, ns&, U&, b() As Byte
Private Sub Class_Initialize()
  Create
End Sub
Public Sub Create(Optional ByVal Content As String, Optional ByVal initialCapacity As Long = 255)
  l = Len(Content): cBuffer = Content: clb = l * 2
  If initialCapacity < l Then U = clb - 1 Else U = initialCapacity * 2 - 1: ReDim Preserve cBuffer(0 To U)
End Sub
Public Sub Append(ByVal value As String)
  lb = LenB(value): If lb = 0 Then Exit Sub
  ns = clb + lb
  If ns > (U + 1) Then U = ns - 1: ReDim Preserve cBuffer(U)
  CopyMemory VarPtr(cBuffer(clb)), StrPtr(value), lb: clb = ns
End Sub

Public Sub join(ByVal Builder As StringBuilder)
  With Builder
    lb = .LenBuffer: If lb = 0 Then Exit Sub
    ns = clb + lb
    If ns > (U + 1) Then U = ns - 1: ReDim Preserve cBuffer(U)
    b = .buffer()
    CopyMemory VarPtr(cBuffer(clb)), VarPtr(b(0)), lb: clb = ns
  End With
End Sub
Public Sub clone(ByVal Builder As StringBuilder)
  With Builder
    clb = .LenBuffer: U = .Capacity: cBuffer = .buffer
  End With
End Sub
Public Property Get buffer(Optional fixLength As Boolean) As Byte()
  If fixLength And clb > 0 Then
    b = cBuffer: ReDim Preserve b(clb - 1): buffer = b
  Else
    buffer = cBuffer:
  End If
End Property
Public Property Get Length() As Long: Length = clb / 2: End Property
Public Property Get LenBuffer() As Long: LenBuffer = clb: End Property
Public Property Get Capacity() As Long: Capacity = U: End Property

Public Sub getString(refString$)
  If clb > 0 Then b = cBuffer: ReDim Preserve b(clb - 1): refString = b Else refString = vbNullString
End Sub
Public Function ToString() As String
'add:Attribute ToString.VB_UserMemId = 0
  If clb > 0 Then b = cBuffer: ReDim Preserve b(clb - 1): ToString = b
End Function
Sub reset()
  clb = 0: ReDim cBuffer(0 To U)
End Sub
Liệu hiệu năng có bị ảnh hưởng không khi trong code bạn sử dụng ReDim Preserve để mở rộng mảng (thực chất là tạo một mảng mới, sao chép nội dung từ mảng cũ sang) và số lượng chuỗi cần xử lý vừa nhiều lại vừa khá dài?
Bạn tham khảo bài viết này xem sao, nội dung khá hay, nói chung là có bắt chước, mô phỏng theo class System.Text.StringBuilder của .NET.
[VB6] StringBuilder - Fast string concatenation
Bài đã được tự động gộp:

Đã lập trình C/C++ thì tận dụng các hàm xử lý bộ nhớ và kernel mạnh mẽ nhất, để đạt được hiệu suất, tối ưu, hiệu quả quản lý bộ nhớ. Toán tử + buộc cấp phát bộ nhớ mới, đường nào mà nó không gọi đến các api SysAllocString,... khi xử lý chuỗi, nó chỉ nhanh khi + 1, 2 lần. memcpy nó là một phần của lập trình cấp thấp, nên lệnh này rất quan trọng, không thể bỏ qua được.
Trong C/C++ làm gì có chuyện dễ dàng dùng mỗi toán tử "+" để nối hai chuỗi vậy bạn, tất cả đều phải làm việc ở dạng mảng, tùy thuộc vào chuỗi đang tương tác là kiểu char hay kiểu wchar_t thì mỗi ký tự chiếm 1 hoặc 2 byte bộ nhớ và tất cả đều phải đánh dấu kết thúc bằng ký tự '\0' (hay còn gọi là null-terminated string). Thư viện hàm C cung cấp nhiều hàm xử lý kiểu chuỗi khác nhau, hàm có tiền tố "str" (vd: strlen) dùng cho mảng char, còn "wcs" (vd: wcslen) dùng cho mảng wchar_t.
 

File đính kèm

Lần chỉnh sửa cuối:
Liệu hiệu năng có bị ảnh hưởng không khi trong code bạn sử dụng ReDim Preserve để mở rộng mảng (thực chất là tạo một mảng mới, sao chép nội dung từ mảng cũ sang) và số lượng chuỗi cần xử lý vừa nhiều lại vừa khá dài?
Bạn tham khảo bài viết này xem sao, nội dung khá hay, nói chung là có bắt chước, mô phỏng theo class System.Text.StringBuilder của .NET.
[VB6] StringBuilder - Fast string concatenation
Bài đã được tự động gộp:


Trong C/C++ làm gì có chuyện dễ dàng dùng mỗi toán tử "+" để nối hai chuỗi vậy bạn, tất cả đều phải làm việc ở dạng mảng, tùy thuộc vào chuỗi đang tương tác là kiểu char hay kiểu wchar_t thì mỗi ký tự chiếm 1 hoặc 2 byte bộ nhớ và tất cả đều phải đánh dấu kết thúc bằng ký tự '\0' (hay còn gọi là null-terminated string). Thư viện hàm C cung cấp nhiều hàm xử lý kiểu chuỗi khác nhau, hàm có tiền tố "str" (vd: strlen) dùng cho mảng char, còn "wcs" (vd: wcslen) dùng cho mảng wchar_t.

tui thấy của bạn @Hesanbe cũng tối ưu đấy chứ. Bạn ấy chỉ điều chỉnh kích thước mảng byte khi vượt quá lưu lược cho trước.
 
ReDim Preserve Là cấp phát bộ nhớ động, không phải sao chép mảng. Trong VBA lệnh này đồng thời cũng là lệnh cấp phát bộ nhớ nhanh hơn các lệnh API.
Trong lớp ở trên lệnh chỉ được gọi, chỉ xảy ra khi bộ nhớ ban đầu nhỏ hơn bộ nhớ đủ để chứa chuỗi.

Nếu đã xác định chuỗi ban đầu lớn chỉ cần gọi Create với bộ nhớ ban đầu lớn là được.

Trong C/C++ cũng có gì ghê gớm đâu, lớp StringBuilder này họ đã phát triển tối ưu, từ xửa từ xưa rồi, giờ chỉ cần kế thừa là xong rồi.
Thời gian mà bỏ ra để viết lại lớp này, rồi thời gian đâu để viết ra ứng dụng.
 
ReDim Preserve Là cấp phát bộ nhớ động, không phải sao chép mảng. Trong VBA lệnh này đồng thời cũng là lệnh cấp phát bộ nhớ nhanh hơn các lệnh API.
Trong lớp ở trên lệnh chỉ được gọi, chỉ xảy ra khi bộ nhớ ban đầu nhỏ hơn bộ nhớ đủ để chứa chuỗi.
ReDim Preserve nhanh hơn so với mấy hàm API như thế nào?
Vậy tóm lại theo bạn, ReDim Preserve hoạt động như thế nào?
Đưa lên trước câu trả lời tham khảo: Does redim use the heap or the stack memory in VBA?
Tôi chưa từng thấy mảng SAFEARRAY có khả năng cấp phát lại (reallocate) để mở rộng kích thước mà vẫn giữ lại phần tử hiện có trong mảng, ở đây chỉ có hàm SafeArrayCopy function (oleauto.h)SafeArrayCopyData function (oleauto.h) sao chép mảng này sang mảng khác.
Trong C/C++ cũng có gì ghê gớm đâu, lớp StringBuilder này họ đã phát triển tối ưu, từ xửa từ xưa rồi, giờ chỉ cần kế thừa là xong rồi.
Thời gian mà bỏ ra để viết lại lớp này, rồi thời gian đâu để viết ra ứng dụng.
Trong Thư viện Tiêu bản Chuẩn (STL) của C/C++ làm gì có lớp gọi là StringBuilder? Tôi thắc mắc bạn thật sự có biết gì về C/C++ không đấy.
Đã lập trình C/C++ thì tận dụng các hàm xử lý bộ nhớ và kernel mạnh mẽ nhất, để đạt được hiệu suất, tối ưu, hiệu quả quản lý bộ nhớ. Toán tử + buộc cấp phát bộ nhớ mới, đường nào mà nó không gọi đến các api SysAllocString,... khi xử lý chuỗi, nó chỉ nhanh khi + 1, 2 lần. memcpy nó là một phần của lập trình cấp thấp, nên lệnh này rất quan trọng, không thể bỏ qua được.
Nếu có khả năng thì cứ chứng minh cho mọi người cùng biết, chứ cứ nói suông đao to búa lớn để làm gì?
 
Làm thế nào mà lại hướng dẫn tạo tham chiếu đường dẫn tuyệt đối cho API kiểu này được bạn.
"Y:\data\CPlusPlus\SimpleDll\x64\Release\SimpleDll.dll"
Rồi người ta tải về sử dụng bằng cách nào để có được đường dẫn này. Cách lập trình này chỉ dành cho chính lập trình viên sử dụng mà thôi.

Nên hướng dẫn theo hướng sử dụng các API là LoadLibrary và DispCallFunc.
Hướng dẫn sai lầm dễ làm con người ta u mê mãi không thoát ra được cái vòng luẩn quẩn của lập trình.
Tôi thấy hướng dẫn như vậy có sai lầm gì đâu nhỉ?
Để đường dẫn tuyệt đối cũng oke mà, chắc thiếu comment cho dòng đó thôi, vd như vậy càng dễ hiểu cho người chưa biết chứ nhỉ?
Bởi đây là bài viết ngắn, chủ yểu nói việc VBA có thể khai báo, sử dụng dll và giới thiệu 1 công cụ tạo dll.
 

Bài viết mới nhất

Back
Top Bottom