Thử lập trình XLL add-in bằng C/C++ (3 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 thường trực
    Tham gia
    25/5/22
    Bài viết
    258
    Được thích
    203
    Có lẽ rất nhiều người dùng đã quen với các dạng add-in mà Excel hỗ trợ, những dạng phổ biến hiện nay (không tính những add-in chỉ dùng cho các bản Office cũ hơn bản 2007) bao gồm .xlam và .xll, trong đó .xlam được viết bằng VBA, còn .xll được viết bằng C/C++ và một số ngôn ngữ lập trình khác như C#, Delphi, v.v., bất cứ ngôn ngữ lập trình nào hỗ trợ khả năng xuất ra DLL tiêu chuẩn (Standard DLL). Với .xlam, người dùng có thể tương tác sâu rộng với Excel cũng như có thể viết hàm tùy chỉnh (hoặc hàm do người dùng tự định nghĩa, user-defined function) để sử dụng trong bảng tính, trong khi .xll chỉ hỗ trợ khả năng tương tác hạn chế với Excel thông qua Excel C API , ngoài ra loại add-in này còn cho phép người dùng tạo ra sheet macro và dialog (những tính năng này đều đã lỗi thời và đã bị macro VBA thay thế từ rất lâu), bên cạnh đó .xll cũng cho phép người dùng tạo ra các hàm tùy chỉnh để dùng trong bảng tính và hiệu năng mà nó mang lại vượt trội so với .xlam của VBA.
    Để lập trình một .xll add-in bằng C/C++, người dùng cần đáp ứng những tiêu chí sau đây:
    • Hiểu biết cơ bản về C/C++.
    • Đã cài đặt Excel Software Development Kit (tải xuống ở đây).
    Để viết mã C/C++, người dùng có thể chọn môi trường lập trình tích hợp (IDE) bất kỳ, ở đây tôi chọn Visual Studio bởi vì đã quen với giao diện của nó cũng như làm việc nhiều với Visual C++.
    Đầu tiên, người dùng mở Visual Studio, chọn Create New Project, sau đó chọn tiếp Dynamic-Link Library (DLL) và lưu lại dự án mới.
    1740587660860.png
    Tiếp theo, tiến hành cài đặt Excel Software Development Kit, đường dẫn mặc định là C:\2013 Office System Developer Resources, sau đó sao chép những tệp sau đây vào thư mục của dự án vừa mới tạo ở trên:
    1. Excel2013XLLSDK\INCLUDE:
    • XLCALL.H
    2. Excel2013XLLSDK\LIB\x64 (với Excel bản 64bit), hoặc Excel2013XLLSDK\LIB (với Excel bản 32bit):
    • XLCALL32.LIB
    3. Excel2013XLLSDK\SAMPLES\FRAMEWRK:
    • FRAMEWRK.C
    • FRAMEWRK.H
    • MemoryManager.cpp
    • MemoryManager.h
    • MemoryPool.cpp
    • MemoryPool.h
    4. Excel2013XLLSDK\SRC:
    • XLCALL.CPP
    1740588008767.png
    Tiến hành viết code, ở đây viết một hàm tên là PrintHello với một tham số duy nhất kiểu chuỗi, nó chỉ có tác dụng in ra dòng chữ "Hello, <tham_số>". Lưu ý, do từ phiên bản Excel 2007 trở đi đều có tính năng Multithreaded Recalculation và Multithreaded Calculation cho phép Excel tận dụng số nhân xử lý của CPU để thực hiện việc tính toán, cho nên tiện lợi nhất là người dùng nên sử dụng Thread-local Storage API nhằm tránh những rắc rối như race condition (điều kiện tranh giành) do tính toán đa luồng gây ra khiến cho kết quả tính toán bị sai lệch.
    C++:
    #include "XLCALL.H"
    #include "FRAMEWRK.H"
    #include "stdlib.h"
    #include "string"
    #include "Windows.h"
    
    DWORD tlsIndex; //Khai báo một biến DWORD để lưu trữ chỉ số của Thread-local Storage mà các thread có thể sử dụng để lưu trữ dữ liệu cục bộ cho mỗi thread
    
    //Khai báo struct để lưu trữ dữ liệu cục bộ cho mỗi thread
    struct TlsData {
        XLOPER12 data;
    };
    
    //Hàm này trả về struct từ Thread-local Storage mà thread hiện tại đang sử dụng
    TlsData* GetTlsData() {
        if (!tlsIndex) return NULL;
        TlsData* tlsData = reinterpret_cast<TlsData*>(TlsGetValue(tlsIndex));
        return tlsData;
    }
    
    //Khởi tạo Thread-local Storage
    BOOL InitializeTls(DWORD DllMainCallReason) {
        switch (DllMainCallReason) {
        case DLL_PROCESS_ATTACH:
            if ((tlsIndex = TlsAlloc()) == TLS_OUT_OF_INDEXES)
                return FALSE;
        case DLL_PROCESS_DETACH:
            if (tlsIndex) {
                TlsFree(tlsIndex);
                break;
            }
        }
        return TRUE;
    }
    
    BOOL APIENTRY DllMain( HMODULE hModule,
                           DWORD  ul_reason_for_call,
                           LPVOID lpReserved
                         )
    {
        //Khởi tạo Thread-local Storage
        return InitializeTls(ul_reason_for_call);
    }
    
    //Hàm này chuyển chuỗi ký tự wide char sang định dạng mà Excel yêu cầu
    wchar_t *CreateCountedUnicodeWcharStringFromNullTerminatedUnicodeWCharString(const wchar_t* text) {
        size_t len; //Khai báo biến chứa độ dài của chuỗi cần chuyển đổi
        if (!text || !(len = wcslen(text))) return NULL; //Nếu chuỗi rỗng thì trả về NULL
        if (len > 32767u) len = 32767u; //Nếu độ dài chuỗi lớn hơn 32767u ký tự thì cắt gọt chuỗi
        wchar_t *wstr = new wchar_t[(len + 2)]; //Cấp phát bộ nhớ động cho chuỗi kết quả, số lượng byte cần cấp phát là độ dài chuỗi cần chuyển đổi cộng thêm 2 ký tự (để dành ký tự cuối cùng làm ký tự kết thúc chuỗi) * 2 byte
        if (!wstr) return NULL; //Nếu không cấp phát được bộ nhớ thì trả về NULL
        memcpy(wstr + 1, text, (len + 1) * sizeof(wchar_t)); //Sao chép chuỗi cần chuyển đổi, bắt đầu từ ký tự thứ hai, vào chuỗi kết quả
        wstr[0] = len; //Gán độ dài chuỗi vào vị trí đầu tiên của chuỗi kết quả
        return wstr; //Trả về chuỗi kết quả
    }
    
    extern "C" __declspec(dllexport) LPXLOPER12 WINAPI PrintHello(wchar_t* name) {
        //static LPXLOPER12 result; Không an toàn thread vì biến tĩnh này có thể bị ghi đè bởi các thread khác
        TlsData* tlsData = GetTlsData(); //Thay vào đó, sử dụng Thread-local Storage API để lưu trữ dữ liệu cục bộ cho mỗi thread
        if (!tlsData) return NULL;
        LPXLOPER12 result = &(tlsData->data); //Khai báo biến con trỏ của kiểu XLOPER12 để lưu trữ kết quả trả về
        result->xltype = xltypeStr | xlbitDLLFree; //Thiết lập kiểu trả về của hàm là chuỗi, đồng thời thông báo cho Excel biết rằng Excel sẽ giải phóng bộ nhớ cho chuỗi này do nó được cấp phát động
        wchar_t wstrInput[100] = L"Hello, "; //Khai báo một mảng ký tự wide char để lưu trữ chuỗi kết quả
        wcscat_s(wstrInput, name); //Nối chuỗi name vào chuỗi wstrInput
        wchar_t* pWstrOutput = CreateCountedUnicodeWcharStringFromNullTerminatedUnicodeWCharString(wstrInput); //Chuyển chuỗi wstrInput sang định dạng mà Excel yêu cầu
        result->val.str = pWstrOutput; //Gán địa chỉ của chuỗi kết quả vào biến con trỏ result
        return result; //Trả về con trỏ result để Excel có thể sử dụng kết quả
    }
    //Excel gọi hàm này khi nạp add-in
    extern "C" __declspec(dllexport) int WINAPI xlAutoOpen(void)
    {
        XLOPER12 xDLL;
        //Xác định tên của DLL
        if (Excel12f(xlGetName, &xDLL, 0) == xlretSuccess) {
            Excel12f(xlfRegister, 0, 11, (LPXLOPER12)&xDLL, //Đăng ký tên của DLL
                (LPXLOPER12)TempStr12(L"PrintHello"), //Tên hàm
                (LPXLOPER12)TempStr12(L"UC%"), //Kiểu tham số, ký tự đầu tiên đại diện cho kiểu dữ liệu trả về của hàm, các ký tự tiếp theo là kiểu dữ liệu của các tham số
                (LPXLOPER12)TempStr12(L"PrintHello"), //Tên hàm trong trình Function Wizard
                (LPXLOPER12)TempStr12(L"name"), //Mô tả tham số trong trình Function Wizard
                (LPXLOPER12)TempStr12(L"1"), //Kiểu macro
                (LPXLOPER12)TempStr12(L"myOwnCppFunctions"), //Hạng mục
                (LPXLOPER12)TempStr12(L""), //Tổ hợp phím tắt
                (LPXLOPER12)TempStr12(L""), //Tên tập tin Help
                (LPXLOPER12)TempStr12(L"Print 'Hello <user>'"), //Mô tả hàm khi được chọn trong trình Function Wizard
                (LPXLOPER12)TempStr12(L"")); //Mô tả tham số khi được chọn trong trình Function Wizard (tối đa lên đến 255 tham số)
            //Giải phóng biến xDLL
            Excel12f(xlFree, 0, 1, (LPXLOPER12)&xDLL);
        }
        return 1;
    }
    
    //Excel gọi hàm này khi người dùng tắt Add-in hoặc thoát khỏi Excel
    extern "C" __declspec(dllexport) int WINAPI xlAutoClose(void) {
        return 1;
    }
    
    //Excel gọi hàm này khi kiểu XLOPER12 trả về có chứa cờ bit xlbitDLLFree hoặc xlbitXLFree để dọn dẹp bộ nhớ
    extern "C" __declspec(dllexport) void WINAPI xlAutoFree12(LPXLOPER12 p_oper) {
        //Nếu kiểu dữ liệu trả về là chuỗi
        if (p_oper->xltype & xltypeStr) {
            delete p_oper->val.str; //Giải phóng bộ nhớ đã cấp phát động cho chuỗi
        }
    }

    Tiến hành biên dịch ra DLL, sau đó đổi phần mở rộng thành .xll và đưa vào Excel.
    1740589448063.png
    Kiểm tra add-in đã được Excel nạp vào.
    1740589490521.png
    Viết thử hàm.
    1740589526459.png
    Để biết thêm thông tin chi tiết về những tính năng mà .xll add-in hỗ trợ, tham khảo bài viết Programming with the C API in Excel
     
    Lần chỉnh sửa cuối:
    Cảm ơn bạn về bài viết này, bạn có thể giúp mình làm 1 video về việc tạo add-in vào excel từ C++ không, cảm ơn bạn.
     
    Cảm ơn bạn về bài viết này, bạn có thể giúp mình làm 1 video về việc tạo add-in vào excel từ C++ không, cảm ơn bạn.
    XLL add-in thực chất vẫn là Standard DLL, chỉ là DLL đó cần hỗ trợ một số hàm bắt buộc để Excel gọi và nhận diện add-in.
    Nếu bạn muốn lập trình XLL add-in bằng C/C++ thì có thể tham khảo cuốn sách này xem sao.
    1754736528882.png
     
    Bạn có thể gửi giúp mình file visual studio C++ theo bài post bên trên được không ah, mình có làm theo nhưng không được chạy được code. Cảm ơn bạn
    nguyentruongtho1711@gmail.com
     
    Lần chỉnh sửa cuối:
    Hàm bất đồng bộ (Asynchronous User-Defined Functions)
    Trong một số trường hợp, hàm có thể mất nhiều thời gian hơn để xử lý và trả kết quả (vd: Đợi thông tin từ internet hoặc khối lượng công việc tính toán lớn mất nhiều thời gian), cho nên khi hàm chạy đồng bộ, Excel phải chờ hàm trả về kết quả xong thì mới cho phép người dùng tương tác với giao diện người dùng (UI) của nó.
    Để giải quyết vấn đề này, Excel giới thiệu mô hình hàm bất đồng bộ, trong đó trình tự các bước thực thi như sau:
    • Đăng ký hàm là hàm bất đồng bộ, hàm này bắt buộc phải trả về kiểu void và phải có tham số chứa kiểu XLOPER hoặc XLOPER12 để Excel sử dụng làm điều khiển (handle) quản lý hàm bất đồng bộ. Tham khảo thêm: xlfRegister (Form 1)
    • Trong hàm, tiến hành khởi tạo thread để xử lý các công việc chính của hàm (mục đích chính của hàm bất đồng bộ), chuẩn bị các dữ liệu cần thiết cho việc tính toán (lưu ý: Các giá trị khi truyền vào thread bắt buộc phải dưới dạng sao chép sâu (deep copy) và được lưu trữ trên vùng nhớ heap bởi vì nếu chỉ lưu trữ trên vùng nhớ stack, khi hàm chính kết thúc thì dữ liệu trên vùng nhớ stack sẽ bị xóa và thu hồi, dẫn đến thread của hàm tham chiếu đến vùng nhớ không hợp lệ và khiến cho Excel bị sập (crash)).
    • Từ thread của hàm, tiến hành tính toán như bình thường, sau khi có kết quả thì dùng hàm gọi lại (callback) xlAsyncReturn để trả kết quả cho Excel.
    C++:
    void AsyncFuncCallback(LPXLOPER12 lpHandle) {
        //Giả lập công việc tính toán mất nhiều thời gian
        Sleep(3000);
        //Chuẩn bị kết quả để trả về cho Excel
        XLOPER12 result = {};
        result.xltype = xltypeNum;
        result.val.num = 10;
        //Trả kết quả về cho Excel
        Excel12f(xlAsyncReturn, NULL, 2, lpHandle, &result);
    }
    
    DWORD WINAPI AsyncFuncCallbackProc(LPVOID lpParam) {
        //Kiểm tra điều kiện đầu vào
        if (!lpParam) return 1;
        LPXLOPER12 pHandle = (LPXLOPER12)lpParam;
        //Gọi hàm tính toán
        AsyncFuncCallback(pHandle);
        return 0;
    }
    
    extern "C" __declspec(dllexport) void WINAPI AsyncFunc(LPXLOPER12 lpHandle) {
        //Kiểm tra điều kiện đầu vào
        if (!lpHandle) return;
        //Tạo thread để tính toán
        HANDLE hThread = CreateThread(NULL, 0, AsyncFuncCallbackProc, lpHandle, 0, NULL);
        if (!hThread) return;
    }
    
    extern "C" __declspec(dllexport) int WINAPI xlAutoOpen()
    {
        XLOPER12 xlDllName;
        //Xác định tên của DLL
        if (Excel12f(xlGetName, &xlDllName, 0) == xlretSuccess) {
            //Đăng ký hàm với Excel
            Excel12f(xlfRegister, NULL, 10, (LPXLOPER12)&xlDllName, //Đăng ký tên của DLL
                (LPXLOPER12)TempStr12(L"AsyncFunc"), //Tên hàm
                (LPXLOPER12)TempStr12(L">X"), //Kiểu tham số, ký tự đầu tiên đại diện cho kiểu dữ liệu trả về của hàm, các ký tự tiếp theo là kiểu dữ liệu của các tham số
                (LPXLOPER12)TempStr12(L"AsyncFunc"), //Tên hàm trong trình Function Wizard
                (LPXLOPER12)TempStr12(L""), //Mô tả tham số trong trình Function Wizard
                (LPXLOPER12)TempStr12(L"1"), //Kiểu macro
                (LPXLOPER12)TempStr12(L"Asynchronous Functions"), //Hạng mục
                (LPXLOPER12)TempStr12(L""), //Tổ hợp phím tắt
                (LPXLOPER12)TempStr12(L""), //Tên tập tin Help
                (LPXLOPER12)TempStr12(L""));
            //Giải phóng bộ nhớ
            Excel12f(xlFree, NULL, 1, (LPXLOPER12)&xlDllName);
        }
        return 1;
    }

    Khi hàm chạy trên bảng tính, điều khiển được trả về ngay cho Excel giúp cho giao diện người dùng không bị đơ, trên ô bảng tính sẽ hiện lỗi #CALC! tức là hàm đang bận tính toán.

    1758287064865.png

    Hàm sau khi chạy xong sẽ trả kết quả về cho Excel.

    1758287137042.png
     
    Mới viết xong XLL tham gia chơi với cho Vui bản 64 bít

    1782809934579.png
     

    File đính kèm

    Bà tám với ChatGPT kết luận như sau

    1782870793970.png

    Thực tế Mạnh tự định nghĩa như sau

    XLL menu Ribbon.png

    Ms không hổ trợ là lý thuyết còn thực tế thì tư duy logis tí là xong
    1782872617839.png
     
    Lần chỉnh sửa cuối:
    Bài viết này chỉ quăng mỗi mã lên đây thì hơi nhàm chán, nếu có thêm người quan tâm phân tích những khía cạnh kỹ thuật liên quan thì sẽ hay hơn.
    P/s: Đang nói bài #1.
     

    Xin chào thế giới​

    Xem xét chữ ký của một hàm sau đây, với mục đích là trả về chuỗi “Hello, World” khi người dùng gõ hàm này vào bảng tính trên Excel:

    C++:
    extern "C" __declspec(dllexport) wchar_t* WINAPI PrintHelloWorld();

    Để Excel có thể nhận diện được hàm này, trước tiên cần phải đăng ký hàm với Excel đã. Để đăng ký hàm với Excel, người dùng cần gọi hàm xlfRegister, thông tin chi tiết về hàm này người dùng có thể tìm thấy ở đây: xlfRegister (Form 1) | Microsoft Learn

    1784205096282.png
    1784205104139.png

    Chú ý đến đối số pxTypeText, tham số cho Excel biết hàm được đăng ký có bao nhiêu tham số, kiểu giá trị trả về và kiểu giá trị của các tham số là gì, ở đây Excel yêu cầu hai loại chuỗi:
    • Null-terminated Unicode wide-character string, tức là chuỗi wchar_t thông thường kết thúc bằng ký tự rỗng (null).
    • Counted Unicode wide-character string, tức là chuỗi wchar_t giống như trên, nhưng khác biệt ở chỗ chuỗi có chứa thông tin về độ dài chuỗi, thông tin này được lưu trữ ở phần tử đầu tiên trong mảng chuỗi.
    Ở đây hàm PrintHelloWorld chọn trả về chuỗi wchar_t thông thường, cho nên ký tự đại diện sẽ là C%. Với việc hàm này không có đối số nên đối số pxTypeText cuối cùng sẽ là “C%”.

    Như vậy nội dung của hàm PrintHelloWorld có thể được viết như sau:

    C++:
    extern "C" __declspec(dllexport) wchar_t* WINAPI PrintHelloWorld() {
        const wchar_t szInput[] = L"Hello World";
        size_t len = wcslen(szInput);
        wchar_t* pwszResult = new wchar_t[len + 1];
        if (!pwszResult) return NULL;
        errno_t err = wcscpy_s(pwszResult, len + 1, szInput);
        if (err) {
            delete[] pwszResult;
            return NULL;
        }
        return pwszResult;
    }

    Thường thì người ta sẽ muốn đăng ký hàm ngay khi Excel nạp XLL, cho nên hàm xlAutoOpen sẽ là nơi lý tưởng nhất để làm việc này bởi vì Excel sẽ gọi hàm này khi vừa mới nạp XLL.

    Sau đây là mã đăng ký hàm:

    C++:
    extern "C" __declspec(dllexport) int WINAPI xlAutoOpen() {
        XLOPER12 xlDllName;
        int iResult = Excel12(xlGetName, &xlDllName, 0);
        if (iResult == xlretFailed) return 0;
        iResult = Excel12(xlfRegister, NULL, 11,
            &xlDllName,
            TempStr12(L"PrintHelloWorld"),
            TempStr12(L"C%"),
            TempStr12(L"PrintHelloWorld"),
            TempStr12(L""),
            TempStr12(L"1"),
            TempStr12(L"UDF"),
            TempStr12(L""),
            TempStr12(L""),
            TempStr12(L"PrintHelloWorld"),
            TempStr12(L"")
        );
        Excel12(xlFree, NULL, 1, &xlDllName);
        return iResult == xlretSuccess ? 1 : 0;
    }

    Hàm khi được sử dụng trên bảng tính:

    1784205174863.png

    Lưu ý: Do hàm trả trực tiếp con trỏ wchar_t* về cho Excel, cho nên sẽ xảy ra tình trạng rò rỉ bộ nhớ sau mỗi lần gọi. Excel sau khi sử dụng xong thì sẽ không giải phóng bộ nhớ mà con trỏ này trỏ đến là vì nó không thể nào biết được vùng nhớ được cấp phát bằng phương thức nào (malloc, new, GlobalAlloc, LocalAlloc, HeapAlloc, v.v...) để giải phóng bằng phương thức tương ứng. Cho nên thường thì người ta sẽ trả về giá trị cho Excel thông qua cấu trúc (struct) XLOPER12 và sử dụng hàm gọi lại xlAutoFree12 để giải phóng vùng nhớ mà cấu trúc này chứa con trỏ trỏ đến.
     

    File đính kèm

    Lần chỉnh sửa cuối:

    Khắc phục vấn đề còn tồn đọng: Rò rỉ bộ nhớ​

    Như đã đề cập ở bài trước, hàm PrintHelloWorld trả con trỏ wchar_t* về cho Excel, cho nên tình trạng rò rỉ bộ nhớ sẽ xảy ra mỗi khi Excel gọi đến hàm này. Để khắc phục tình trạng này, người dùng nên sử dụng cấu trúc (struct) XLOPER12 để gửi gắm giá trị cần trả về cho Excel, cấu trúc này gồm nhiều hợp (union) của các cấu trúc khác nhau.

    C++:
    /*
    ** XLOPER12 structure
    **
    ** Excel 12's fundamental data type: can hold data
    ** of any type. Use "U" as the argument type in the
    ** REGISTER function.
    **/
    
    typedef struct xloper12
    {
        union
        {
            double num;                           /* xltypeNum */
            XCHAR *str;                           /* xltypeStr */
            BOOL xbool;                           /* xltypeBool */
            int err;                           /* xltypeErr */
            int w;
            struct
            {
                WORD count;                       /* always = 1 */
                XLREF12 ref;
            } sref;                        /* xltypeSRef */
            struct
            {
                XLMREF12 *lpmref;
                IDSHEET idSheet;
            } mref;                        /* xltypeRef */
            struct
            {
                struct xloper12 *lparray;
                RW rows;
                COL columns;
            } array;                    /* xltypeMulti */
            struct
            {
                union
                {
                    int level;            /* xlflowRestart */
                    int tbctrl;            /* xlflowPause */
                    IDSHEET idSheet;        /* xlflowGoto */
                } valflow;
                RW rw;                           /* xlflowGoto */
                COL col;                       /* xlflowGoto */
                BYTE xlflow;
            } flow;                        /* xltypeFlow */
            struct
            {
                union
                {
                    BYTE *lpbData;            /* data passed to XL */
                    HANDLE hdata;            /* data returned from XL */
                } h;
                long cbData;
            } bigdata;                    /* xltypeBigData */
        } val;
        DWORD xltype;
    } XLOPER12, *LPXLOPER12;

    Như vậy để truyền con trỏ wchar_t* cho Excel thông qua XLOPER12, người dùng cần bật cờ bit xltypeStr cho trường xltype để thông báo cho Excel biết hàm muốn trả về giá trị kiểu chuỗi, rồi sau đó chỉ định con trỏ wchar_t* trong cấu trúc val.str, ví dụ:

    C++:
    static XLOPER12 xlRet;
    xlRet.xltype = xltypeStr;
    xlRet.val.str = pwszVal;

    Có một vấn đề thế này, bởi vì Excel đòi hỏi chuỗi được truyền thông qua XLOPER12 phải là chuỗi có chứa thông tin về độ dài chuỗi, cho nên người dùng không thể nào sử dụng chuỗi wchar_t thông thường được. Thay vào đó, người dùng sẽ phải bố trí cấu trúc của chuỗi đúng như những gì mà Excel yêu cầu: Phần tử đầu tiên trong mảng chứa thông tin về độ dài chuỗi, mà phần tử này có kích thước của kiểu wchar_t là 2 byte, cho nên giá trị dương tối đa mà nó có thể biểu diễn là 32768, phần còn lại của mảng là chuỗi như thường dùng. Do phải tốn một phần tử trong mảng để chứa thông tin về độ dài chuỗi, cho nên 32768 – 1 = 32767 độ dài chuỗi tối đa mà Excel có thể tiếp nhận.

    Như vậy có thể viết hàm chuyển đổi chuỗi theo yêu cầu Excel như sau:

    C++:
    #define MAX_XL12_STR_LEN 32767u
    
    wchar_t* WideCharToXl12Str(const wchar_t* lpwszInput) {
        size_t len;
        if (!lpwszInput || !(len = wcslen(lpwszInput))) return NULL;
        if (len > MAX_XL12_STR_LEN) len = MAX_XL12_STR_LEN;
        wchar_t* pwszResult = new wchar_t[len + 2];
        if (!pwszResult) return NULL;
        pwszResult[0] = (wchar_t)len;
        errno_t err = wcscpy_s(pwszResult + 1, len + 1, lpwszInput);
        if (err) {
            delete[] pwszResult;
            return NULL;
        }
        return pwszResult;
    }

    Sửa lại hàm PrintHelloWorld như sau:

    C++:
    extern "C" __declspec(dllexport) LPXLOPER12 WINAPI PrintHelloWorld() {
        const wchar_t szInput[] = L"Hello World";
        static XLOPER12 xlRet;
        xlRet.xltype = xltypeStr | xlbitDLLFree;
        xlRet.val.str = WideCharToXl12Str(szInput);
        if (!xlRet.val.str) return NULL;
        return &xlRet;
    }

    Sửa lại phần đăng ký hàm với Excel:

    C++:
    extern "C" __declspec(dllexport) int WINAPI xlAutoOpen() {
        XLOPER12 xlDllName;
        int iResult = Excel12(xlGetName, &xlDllName, 0);
        if (iResult == xlretFailed) return 0;
        iResult = Excel12(xlfRegister, NULL, 11,
            &xlDllName,
            TempStr12(L"PrintHelloWorld"),
            TempStr12(L"Q"),
            TempStr12(L"PrintHelloWorld"),
            TempStr12(L""),
            TempStr12(L"1"),
            TempStr12(L"UDF"),
            TempStr12(L""),
            TempStr12(L""),
            TempStr12(L"PrintHelloWorld"),
            TempStr12(L"")
        );
        Excel12(xlFree, NULL, 1, &xlDllName);
        return iResult == xlretSuccess ? 1 : 0;
    }

    Quan trọng: Để giải quyết vấn đề rò rỉ bộ nhớ, Excel cung cấp một hàm gọi lại tên là xlAutoFree12, hàm này có một tham số là con trỏ kiểu LPXLOPER12, đây chính là con trỏ mà người dùng đã trả về từ hàm (vd: PrintHelloWorld chẳng hạn). Hàm xlAutoFree12 được gọi khi và chỉ khi người dùng bật cờ bit xlbitDLLFree hoặc xlbitXLFree. Cờ bit xlbitDLLFree được sử dụng khi người dùng cấp phát và sử dụng vùng nhớ theo cách thông thường (như trong hàm PrintHelloWorld) chẳng hạn, còn cờ bit xlbitXLFree được sử dụng khi vùng nhớ đó được tạo ra nhờ kết quả của hàm Excel12 (vd: Trả về từ hàm xlGetName chẳng hạn).

    Nhờ hàm gọi lại xlAutoFree12 mà người dùng sẽ có cơ hội giải phóng bộ nhớ sau khi hàm được thực hiện xong, loại bỏ hoàn toàn vấn đề rò rỉ bộ nhớ.

    C++:
    extern "C" __declspec(dllexport) int WINAPI xlAutoFree12(LPXLOPER12 pOper) {
        if (!pOper) return 0;
        if (pOper->xltype & (xltypeStr | xlbitDLLFree) || pOper->val.str) {
            delete[] pOper->val.str;
        }
        return 0;
    }

    Kiểm tra xem Excel có gọi lại hàm xlAutoFree12 không sau khi thực hiện xong hàm PrintHelloWorld. Kết quả đúng như ý muốn.

    1784257739328.png

    Tạm thời cứ đơn giản như trên đã, sau này muốn tận dụng tính năng Multithreaded Recalculation của Excel, tức là Excel sẽ sử dụng nhiều thread khác nhau để tính toán lại hàm khi có yêu cầu cần tính toán lại trên bảng tính, thì hàm PrintHelloWorld ở trên bắt buộc phải đảm bảo an toàn thread, tuy nhiên khai báo static XLOPER12 thì không được rồi, cho nên sẽ cần phải làm thêm một số việc khác.
     
    Lần chỉnh sửa cuối:

    Khắc phục vấn đề còn tồn đọng: Rò rỉ bộ nhớ​

    Như đã đề cập ở bài trước, hàm PrintHelloWorld trả con trỏ wchar_t* về cho Excel, cho nên tình trạng rò rỉ bộ nhớ sẽ xảy ra mỗi khi Excel gọi đến hàm này. Để khắc phục tình trạng này, người dùng nên sử dụng cấu trúc (struct) XLOPER12 để gửi gắm giá trị cần trả về cho Excel, cấu trúc này gồm nhiều hợp (union) của các cấu trúc khác nhau.

    C++:
    /*
    ** XLOPER12 structure
    **
    ** Excel 12's fundamental data type: can hold data
    ** of any type. Use "U" as the argument type in the
    ** REGISTER function.
    **/
    
    typedef struct xloper12
    {
        union
        {
            double num;                           /* xltypeNum */
            XCHAR *str;                           /* xltypeStr */
            BOOL xbool;                           /* xltypeBool */
            int err;                           /* xltypeErr */
            int w;
            struct
            {
                WORD count;                       /* always = 1 */
                XLREF12 ref;
            } sref;                        /* xltypeSRef */
            struct
            {
                XLMREF12 *lpmref;
                IDSHEET idSheet;
            } mref;                        /* xltypeRef */
            struct
            {
                struct xloper12 *lparray;
                RW rows;
                COL columns;
            } array;                    /* xltypeMulti */
            struct
            {
                union
                {
                    int level;            /* xlflowRestart */
                    int tbctrl;            /* xlflowPause */
                    IDSHEET idSheet;        /* xlflowGoto */
                } valflow;
                RW rw;                           /* xlflowGoto */
                COL col;                       /* xlflowGoto */
                BYTE xlflow;
            } flow;                        /* xltypeFlow */
            struct
            {
                union
                {
                    BYTE *lpbData;            /* data passed to XL */
                    HANDLE hdata;            /* data returned from XL */
                } h;
                long cbData;
            } bigdata;                    /* xltypeBigData */
        } val;
        DWORD xltype;
    } XLOPER12, *LPXLOPER12;

    Như vậy để truyền con trỏ wchar_t* cho Excel thông qua XLOPER12, người dùng cần bật cờ bit xltypeStr cho trường xltype để thông báo cho Excel biết hàm muốn trả về giá trị kiểu chuỗi, rồi sau đó chỉ định con trỏ wchar_t* trong cấu trúc val.str, ví dụ:

    C++:
    static XLOPER12 xlRet;
    xlRet.xltype = xltypeStr;
    xlRet.val.str = pwszVal;

    Có một vấn đề thế này, bởi vì Excel đòi hỏi chuỗi được truyền thông qua XLOPER12 phải là chuỗi có chứa thông tin về độ dài chuỗi, cho nên người dùng không thể nào sử dụng chuỗi wchar_t thông thường được. Thay vào đó, người dùng sẽ phải bố trí cấu trúc của chuỗi đúng như những gì mà Excel yêu cầu: Phần tử đầu tiên trong mảng chứa thông tin về độ dài chuỗi, mà phần tử này có kích thước của kiểu wchar_t là 2 byte, cho nên giá trị dương tối đa mà nó có thể biểu diễn là 32768, phần còn lại của mảng là chuỗi như thường dùng. Do phải tốn một phần tử trong mảng để chứa thông tin về độ dài chuỗi, cho nên 32768 – 1 = 32767 độ dài chuỗi tối đa mà Excel có thể tiếp nhận.

    Như vậy có thể viết hàm chuyển đổi chuỗi theo yêu cầu Excel như sau:

    C++:
    #define MAX_XL12_STR_LEN 32767u
    
    wchar_t* WideCharToXl12Str(const wchar_t* lpwszInput) {
        size_t len;
        if (!lpwszInput || !(len = wcslen(lpwszInput))) return NULL;
        if (len > MAX_XL12_STR_LEN) len = MAX_XL12_STR_LEN;
        wchar_t* pwszResult = new wchar_t[len + 2];
        if (!pwszResult) return NULL;
        pwszResult[0] = (wchar_t)len;
        errno_t err = wcscpy_s(pwszResult + 1, len + 1, lpwszInput);
        if (err) {
            delete[] pwszResult;
            return NULL;
        }
        return pwszResult;
    }

    Sửa lại hàm PrintHelloWorld như sau:

    C++:
    extern "C" __declspec(dllexport) LPXLOPER12 WINAPI PrintHelloWorld() {
        const wchar_t szInput[] = L"Hello World";
        static XLOPER12 xlRet;
        xlRet.xltype = xltypeStr | xlbitDLLFree;
        xlRet.val.str = WideCharToXl12Str(szInput);
        if (!xlRet.val.str) return NULL;
        return &xlRet;
    }

    Sửa lại phần đăng ký hàm với Excel:

    C++:
    extern "C" __declspec(dllexport) int WINAPI xlAutoOpen() {
        XLOPER12 xlDllName;
        int iResult = Excel12(xlGetName, &xlDllName, 0);
        if (iResult == xlretFailed) return 0;
        iResult = Excel12(xlfRegister, NULL, 11,
            &xlDllName,
            TempStr12(L"PrintHelloWorld"),
            TempStr12(L"Q"),
            TempStr12(L"PrintHelloWorld"),
            TempStr12(L""),
            TempStr12(L"1"),
            TempStr12(L"UDF"),
            TempStr12(L""),
            TempStr12(L""),
            TempStr12(L"PrintHelloWorld"),
            TempStr12(L"")
        );
        Excel12(xlFree, NULL, 1, &xlDllName);
        return iResult == xlretSuccess ? 1 : 0;
    }

    Quan trọng: Để giải quyết vấn đề rò rỉ bộ nhớ, Excel cung cấp một hàm gọi lại tên là xlAutoFree12, hàm này có một tham số là con trỏ kiểu LPXLOPER12, đây chính là con trỏ mà người dùng đã trả về từ hàm (vd: PrintHelloWorld chẳng hạn). Hàm xlAutoFree12 được gọi khi và chỉ khi người dùng bật cờ bit xlbitDLLFree hoặc xlbitXLFree. Cờ bit xlbitDLLFree được sử dụng khi người dùng cấp phát và sử dụng vùng nhớ theo cách thông thường (như trong hàm PrintHelloWorld) chẳng hạn, còn cờ bit xlbitXLFree được sử dụng khi vùng nhớ đó được tạo ra nhờ kết quả của hàm Excel12 (vd: Trả về từ hàm xlGetName chẳng hạn).

    Nhờ hàm gọi lại xlAutoFree12 mà người dùng sẽ có cơ hội giải phóng bộ nhớ sau khi hàm được thực hiện xong, loại bỏ hoàn toàn vấn đề rò rỉ bộ nhớ.

    C++:
    extern "C" __declspec(dllexport) int WINAPI xlAutoFree12(LPXLOPER12 pOper) {
        if (!pOper) return 0;
        if (pOper->xltype & (xltypeStr | xlbitDLLFree) || pOper->val.str) {
            delete[] pOper->val.str;
        }
        return 0;
    }

    Kiểm tra xem Excel có gọi lại hàm xlAutoFree12 không sau khi thực hiện xong hàm PrintHelloWorld. Kết quả đúng như ý muốn.

    View attachment 312075

    Tạm thời cứ đơn giản như trên đã, sau này muốn tận dụng tính năng Multithreaded Recalculation của Excel, tức là Excel sẽ sử dụng nhiều thread khác nhau để tính toán lại hàm khi có yêu cầu cần tính toán lại trên bảng tính, thì hàm PrintHelloWorld ở trên bắt buộc phải đảm bảo an toàn thread, tuy nhiên khai báo static XLOPER12 thì không được rồi, cho nên sẽ cần phải làm thêm một số việc khác.
    mới viết có một hàm PrintHelloWorld thì dòm cái hàm xlAutoOpen đã dài cả mét rồi vậy nếu có 50 hàm với các tham số động thì điều gì xẩy ra

    nó sẽ lên cấp số nhân hay số cộng vậy tạm tính dài lên 50 mét quá ... xong quản lý kiểu gì ???!!!

    đừng buồn từ đó tư duy chút có phương pháp viết thôi ... không lẽ cứ mãi PrintHelloWorld hay Chào thế giới
     
    mới viết có một hàm PrintHelloWorld thì dòm cái hàm xlAutoOpen đã dài cả mét rồi vậy nếu có 50 hàm với các tham số động thì điều gì xẩy ra

    nó sẽ lên cấp số nhân hay số cộng vậy tạm tính dài lên 50 mét quá ... xong quản lý kiểu gì ???!!!

    đừng buồn từ đó tư duy chút có phương pháp viết thôi ... không lẽ cứ mãi PrintHelloWorld hay Chào thế giới
    Buồn cái gì, demo ngắn gọn thì viết thế cho người mới tìm hiểu mới dễ lĩnh ngộ được, chứ nếu muốn ngắn gọn thì viết thêm vài hàm gói gọn tham số là xong, có gì khó.
    Chứ như bên ông dùng Delphi có sẵn thư viện người ta viễn sẵn cho thì nói làm gì.
    Bắt đầu tìm hiểu cái mới thì phải biết viết Hello World, chứ tay mơ đòi nhảy vào phải viết được cái này phải viết được cái kia thì có mà tới mùa quít à.
     
    Buồn cái gì, demo ngắn gọn thì viết thế cho người mới tìm hiểu mới dễ lĩnh ngộ được, chứ nếu muốn ngắn gọn thì viết thêm vài hàm gói gọn tham số là xong, có gì khó.
    Chứ như bên ông dùng Delphi có sẵn thư viện người ta viễn sẵn cho thì nói làm gì.
    Bắt đầu tìm hiểu cái mới thì phải biết viết Hello World, chứ tay mơ đòi nhảy vào phải viết được cái này phải viết được cái kia thì có mà tới mùa quít à.
    Delphi viết cho Excel nói chung XLL hay TaskPane làm gì có hàm nào "Delphi có sẵn thư viện người ta viễn sẵn" mà mọi cái định định nghĩa lấy mà dùng

    nếu nói CSDL như FireDAC họ viết cho SQLite với các hàm bao bọc wrapper thì còn nghe được ... quên chuyện ăn sẵn đi
     
    Delphi viết cho Excel nói chung XLL hay TaskPane làm gì có hàm nào "Delphi có sẵn thư viện người ta viễn sẵn" mà mọi cái định định nghĩa lấy mà dùng

    nếu nói CSDL như FireDAC họ viết cho SQLite với các hàm bao bọc wrapper thì còn nghe được ... quên chuyện ăn sẵn đi
    Rồi ông có dám khẳng định cái XLL ông viết có thông qua một lớp thư viện gói gọn nào không. Excel vốn dĩ ban đầu viết bằng C, nếu không nhờ viết lại mã dựa trên những tệp header .h mà Microsoft cung cấp, nếu không nhờ Delphi ngầm thực hiện việc chuyển đổi kiểu dữ liệu qua lại giữa Delphi với C thì có mà chạy được đấy.
    Hình như tôi thấy ông hơi lạc đề rồi, bài viết này tập trung vào những khía cạnh viết XLL bằng C++ chứ không phải để khoe xem viết bằng Delphi hay C++ thì ngắn hơn, đẳng cấp hơn.
     
    Rồi ông có dám khẳng định cái XLL ông viết có thông qua một lớp thư viện gói gọn nào không. Excel vốn dĩ ban đầu viết bằng C, nếu không nhờ viết lại mã dựa trên những tệp header .h mà Microsoft cung cấp, nếu không nhờ Delphi ngầm thực hiện việc chuyển đổi kiểu dữ liệu qua lại giữa Delphi với C thì có mà chạy được đấy.
    Hình như tôi thấy ông hơi lạc đề rồi, bài viết này tập trung vào những khía cạnh viết XLL bằng C++ chứ không phải để khoe xem viết bằng Delphi hay C++ thì ngắn hơn, đẳng cấp hơn.
    Không lạc đâu còn ai viết trên bất cứ ngôn ngữ nào cũng dựa vào header.h là c++ của Ms xong XLL ... còn chuyện ăn sãn chỉ mấy chú tái sử dụng ví dụ

    như : Excel-DNA hay ADD-IN EXPRESS™ hay bên thứ ba nào đó xong viết XLL thì mới gọi là ăn sẵn

    Rảnh tôi sẽ viết c++ builder cũng thế thôi ... Tôi dừng ở đây không bàn luận nữa vì không cần thiết
     
    Không lạc đâu còn ai viết trên bất cứ ngôn ngữ nào cũng dựa vào header.h là c++ của Ms xong XLL ... còn chuyện ăn sãn chỉ mấy chú tái sử dụng ví dụ

    như : Excel-DNA hay ADD-IN EXPRESS™ hay bên thứ ba nào đó xong viết XLL thì mới gọi là ăn sẵn

    Rảnh tôi sẽ viết c++ builder cũng thế thôi ... Tôi dừng ở đây không bàn luận nữa vì không cần thiết
    Gửi ông mã viết bằng CodeBlocks sử dụng trình biên dịch GCC để tham khảo.

    1784298002836.png

    Nói chung là cú pháp thì vẫn thế thôi, chỉ là khi làm việc bằng IDE khác nhau và trình biên dịch khác nhau thì sẽ có những cách làm việc khác nhau.
     

    File đính kèm

    Gửi ông mã viết bằng CodeBlocks sử dụng trình biên dịch GCC để tham khảo.

    View attachment 312089

    Nói chung là cú pháp thì vẫn thế thôi, chỉ là khi làm việc bằng IDE khác nhau và trình biên dịch khác nhau thì sẽ có những cách làm việc khác nhau.
    Gửi lại ông mã C++ builder tham khảo nè ... cơ bản vậy còn tuỳ biến và triển khai tuỳ ai đó sử dụng và viết

    Tôi loại bỏ sạch Excel4 vì đồ cổ cho Excel 2003 không cần thiết mà sử dụng Excel12 cho sạch và tốt cho viết sau này

    Tôi tải lại File của Ông xong cùng AI viết lại trong 15 phút xong .. chỉ dùng các file cần dùng còn lại xoá hết cho sạch
     

    File đính kèm

    trước khi chưa có AI sợ nhất dịch từ XLCALL.H sang XLCall.pas ... giờ thì cũng quá đơn giản rồi Nhờ AI dịch là ra thôi không còn gì là mật với mỡ nữa

    Mà cái quan trọng là có nó rồi thì ai đó ứng dụng viết được cái gì mới quan trọng ...

    xong tự viết lấy mà dùng chứ không có chuyện ăn sẵn như như ai đó ăn sẵn Excel-DNA , ADD-IN EXPRESS™ hay bên thứ ba nào đó xong viết XLL

    Mã sau do AI dịch ... quá dài nên tôi chỉ cung cấp một phần

    Mã:
    ///***************************************************************************
    // File:        XLCall.pas
    //
    // Muc dich:    Ban dich Pascal (Delphi) day du cua XLCALL.H - header C
    //              cua Microsoft Excel Developer's Toolkit (dinh nghia kieu
    //              du lieu va hang so dung trong Excel C API / XLL).
    //
    // Ghi chu quan trong:
    //              - Day la ban dich 1-1 toan bo hang so (~1094 gia tri) va
    //                cau truc du lieu tu XLCALL.H goc, de dung lau dai cho
    //                nhieu du an XLL viet bang Delphi.
    //              - Khac voi C (header .h rieng, implementation .cpp rieng),
    //                mot unit Delphi gop ca hai phan (interface = header,
    //                implementation = file .cpp) trong CUNG MOT FILE. Do la
    //                quy uoc chuan cua Delphi, khong phai thieu sot.
    //              - Cac ham Excel4()/Excel4v()/XLCallVer()/LPenHelper() la
    //                API Excel4 kieu cu (truoc Excel 2007), KHONG duoc cai
    //                dat trong unit nay vi ban XLCALL.CPP goc cua du an cung
    //                khong co cai dat cho chung (da bi loai bo khi vi ban
    //                C++ Builder chuyen sang chi dung Excel12/XLOPER12). Neu
    //                thuc su can dung API Excel4 cu, ban se can bo sung rieng.
    //              - Excel12() va Excel12v() duoc cai dat day du, dich chinh
    //                xac tu XLCALL.CPP, dung co che GetProcAddress giong het
    //                ban C++.
    //
    // Nen tang:    Microsoft Windows (Win32/Win64)
    //***************************************************************************
    
    unit XLCall;
    
    interface
    
    uses
      Windows;
    
    // -------------------------------------------------------------------------
    // Kieu du lieu co ban XL12 (XL 12 Basic Datatypes)
    // -------------------------------------------------------------------------
    // Ghi chu: khong dinh nghia lai BOOL o day - don vi Windows cua Delphi da
    // co san BOOL = LongBool (Longint 4 byte, 0 = False, khac 0 = True), hoan
    // toan tuong thich voi "typedef INT32 BOOL" trong C.
    type
      XCHAR  = WideChar;              // Wide Character (WCHAR trong C)
      PXCHAR = PWideChar;
      RW     = Integer;               // XL 12 Row  (INT32)
      COL    = Integer;               // XL 12 Column (INT32)
      IDSHEET = NativeUInt;           // XL12 Sheet ID (DWORD_PTR)
    
      // -----------------------------------------------------------------------
      // XLREF - Mo ta MOT tham chieu hinh chu nhat (kieu cu, truoc Excel 2007)
      // -----------------------------------------------------------------------
      PXLREF = ^TXLREF;
      LPXLREF = PXLREF;
      TXLREF = record
        rwFirst:  Word;
        rwLast:   Word;
        colFirst: Byte;
        colLast:  Byte;
      end;
    
      // -----------------------------------------------------------------------
      // XLMREF - Mo ta NHIEU tham chieu hinh chu nhat (kieu cu)
      // Day la cau truc kich thuoc thay doi: reftbl thuc te co "count" phan
      // tu, khong phai 1. Khi cap phat dong, cap phat them (count-1) phan tu
      // XLREF sau cau truc nay.
      // -----------------------------------------------------------------------
      PXLMREF = ^TXLMREF;
      LPXLMREF = PXLMREF;
      TXLMREF = record
        count:  Word;
        reftbl: array[0..0] of TXLREF; // thuc te la reftbl[count]
      end;
    
      // -----------------------------------------------------------------------
      // XLREF12 - Mo ta MOT tham chieu hinh chu nhat (Excel 2007 tro len)
      // -----------------------------------------------------------------------
      PXLREF12 = ^TXLREF12;
      LPXLREF12 = PXLREF12;
      TXLREF12 = record
        rwFirst:  RW;
        rwLast:   RW;
        colFirst: COL;
        colLast:  COL;
      end;
    
      // -----------------------------------------------------------------------
      // XLMREF12 - Mo ta NHIEU tham chieu hinh chu nhat (Excel 2007 tro len)
      // Cung la cau truc kich thuoc thay doi, xem ghi chu cua XLMREF o tren.
      // -----------------------------------------------------------------------
      PXLMREF12 = ^TXLMREF12;
      LPXLMREF12 = PXLMREF12;
      TXLMREF12 = record
        count:  Word;
        reftbl: array[0..0] of TXLREF12; // thuc te la reftbl[count]
      end;
    
      // -----------------------------------------------------------------------
      // FP / FP12 - Mang so thuc 2 chieu (kieu du lieu "P" trong REGISTER)
      // Truong "array" trong C doi ten thanh "arr" vi "array" la tu khoa cua
      // Pascal. Kich thuoc thuc te la rows * columns phan tu.
      // -----------------------------------------------------------------------
      PFP = ^TFP;
      TFP = record
        rows:    Word;
        columns: Word;
        arr:     array[0..0] of Double; // thuc te la array[rows][columns]
      end;
    
      PFP12 = ^TFP12;
      TFP12 = record
        rows:    Integer;
        columns: Integer;
        arr:     array[0..0] of Double; // thuc te la array[rows][columns]
      end;
    
      // =========================================================================
      // XLOPER (kieu cu, truoc Excel 2007) - "R" trong REGISTER
      // =========================================================================
      // Cau truc C goc:
      //   struct xloper { union { ... } val; WORD xltype; };
      //
      // Trong Pascal khong the dat mot truong binh thuong SAU phan variant
      // (case...of), nen ta tach union "val" thanh mot kieu record rieng
      // (TXLOperVal), roi dat no truoc truong xltype trong TXLOPER - dung
      // thu tu bo nho nhu ban C.
      PXLOPER = ^TXLOPER;
      LPXLOPER = PXLOPER;
    
      TXLOperSRef = record
        count: Word;      // luon = 1
        ref:   TXLREF;
      end;
    
      TXLOperMRef = record
        lpmref:  LPXLMREF;
        idSheet: IDSHEET;
      end;
    
      TXLOperArray = record
        lparray: LPXLOPER;
        rows:    Word;
        columns: Word;
      end;
    
      TXLOperFlowValflow = record
        case Integer of
          0: (level:   SmallInt);  // xlflowRestart
          1: (tbctrl:  SmallInt);  // xlflowPause
          2: (idSheet: IDSHEET);   // xlflowGoto
      end;
    
      TXLOperFlow = record
        valflow: TXLOperFlowValflow;
        rw:      Word;   // xlflowGoto
        col:     Byte;    // xlflowGoto
        xlflow:  Byte;
      end;
    
      TXLOperBigDataH = record
        case Integer of
          0: (lpbData: PByte);   // du lieu truyen vao XL
          1: (hdata:   THandle); // du lieu XL tra ve
      end;
    
      TXLOperBigData = record
        h:      TXLOperBigDataH;
        cbData: LongInt;
      end;
    
      TXLOperVal = record
        case Integer of
          0: (num:   Double);          // xltypeNum
          1: (str:   PAnsiChar);       // xltypeStr (chuoi ANSI kieu Pascal-string, byte dau la do dai)
          2: (xbool: Word);            // xltypeBool
          3: (err:   Word);            // xltypeErr
          4: (w:     SmallInt);        // xltypeInt
          5: (sref:  TXLOperSRef);     // xltypeSRef
          6: (mref:  TXLOperMRef);     // xltypeRef
          7: (arr:   TXLOperArray);    // xltypeMulti
          8: (flow:  TXLOperFlow);     // xltypeFlow
          9: (bigdata: TXLOperBigData); // xltypeBigData
      end;
    
      TXLOPER = record
        val:    TXLOperVal;
        xltype: Word;
      end;
    
      // =========================================================================
      // XLOPER12 (Excel 2007 tro len) - "U" trong REGISTER
      // =========================================================================
      // Day la kieu du lieu chinh dung trong toan bo cac ham Excel12 cua du an.
      PXLOPER12 = ^TXLOPER12;
      LPXLOPER12 = PXLOPER12;
    
      TXLOper12SRef = record
        count: Word;       // luon = 1
        ref:   TXLREF12;
      end;
    
      TXLOper12MRef = record
        lpmref:  LPXLMREF12;
        idSheet: IDSHEET;
      end;
    
      TXLOper12Array = record
        lparray: LPXLOPER12;
        rows:    RW;
        columns: COL;
      end;
    
      TXLOper12FlowValflow = record
        case Integer of
          0: (level:   Integer);  // xlflowRestart
          1: (tbctrl:  Integer);  // xlflowPause
          2: (idSheet: IDSHEET);  // xlflowGoto
      end;
    
      TXLOper12Flow = record
        valflow: TXLOper12FlowValflow;
        rw:      RW;    // xlflowGoto
        col:     COL;   // xlflowGoto
        xlflow:  Byte;
      end;
    
      TXLOper12BigDataH = record
        case Integer of
          0: (lpbData: PByte);   // du lieu truyen vao XL
          1: (hdata:   THandle); // du lieu XL tra ve
      end;
    
      TXLOper12BigData = record
        h:      TXLOper12BigDataH;
        cbData: LongInt;
      end;
    
      TXLOper12Val = record
        case Integer of
          0: (num:     Double);         // xltypeNum
          1: (str:     PWideChar);      // xltypeStr (chuoi Unicode kieu Pascal-string, ky tu dau la do dai)
          2: (xbool:   BOOL);           // xltypeBool
          3: (err:     Integer);        // xltypeErr
          4: (w:       Integer);        // xltypeInt
          5: (sref:    TXLOper12SRef);  // xltypeSRef
          6: (mref:    TXLOper12MRef);  // xltypeRef
          7: (arr:     TXLOper12Array); // xltypeMulti
          8: (flow:    TXLOper12Flow);  // xltypeFlow
          9: (bigdata: TXLOper12BigData); // xltypeBigData
      end;
    
      TXLOPER12 = record
        val:    TXLOper12Val;
        xltype: DWORD;
      end;
    
      // -----------------------------------------------------------------------
      // FMLAINFO / MOUSEINFO - dung voi LPenHelper (API cu, xem ghi chu cuoi
      // file). Dich day du de tuong thich kieu du lieu, du hien khong co ham
      // nao trong unit nay su dung truc tiep.
      // -----------------------------------------------------------------------
      PFmlaInfo = ^TFmlaInfo;
      TFmlaInfo = record
        wPointMode: Integer;  // che do soan thao hien tai. 0 => phan con lai chua xac dinh
        cch:        Integer;  // so ky tu trong cong thuc
        lpch:       PAnsiChar; // con tro toi ky tu cong thuc. CHI DOC!!!
        ichFirst:   Integer;  // vi tri bat dau vung chon
        ichLast:    Integer;  // vi tri ket thuc vung chon (co the > cch)
        ichCaret:   Integer;  // vi tri con tro nhap nhay
      end;
    
      PMouseInfo = ^TMouseInfo;
      TMouseInfo = record
        // phan nhap
        hwnd: HWND;   // cua so can lay thong tin
        pt:   TPoint; // vi tri chuot can lay thong tin
        // phan xuat
        dt:  Integer; // loai tai lieu (document/page)
        ht:  Integer; // ma hit-test
        rw:  Integer; // dong tai vi tri chuot (-1 neu khong xac dinh)
        col: Integer; // cot tai vi tri chuot (-1 neu khong xac dinh)
      end;
    
      // -----------------------------------------------------------------------
      // Con tro ham callback bat dong bo cho Cluster Connector (HPC)
      // -----------------------------------------------------------------------
      TXlHpcAsyncCallback = function(operAsyncHandle: LPXLOPER12;
                                      operReturn: LPXLOPER12): Integer; stdcall;
    
    const
     
    Gửi lại ông mã C++ builder tham khảo nè ... cơ bản vậy còn tuỳ biến và triển khai tuỳ ai đó sử dụng và viết

    Tôi loại bỏ sạch Excel4 vì đồ cổ cho Excel 2003 không cần thiết mà sử dụng Excel12 cho sạch và tốt cho viết sau này

    Tôi tải lại File của Ông xong cùng AI viết lại trong 15 phút xong .. chỉ dùng các file cần dùng còn lại xoá hết cho sạch
    Như vầy là ổn rồi, là demo nên tạm thời cứ như thế đã, sau này khi bài toán phức tạp hơn thì gánh nặng công việc cũng sẽ tăng lên, về quản lý bộ nhớ, về việc đảm bảo an toàn thread cho hàm để Excel có thể sử dụng cho Multithreaded Recalculation, v.v...
     
    Như vầy là ổn rồi, là demo nên tạm thời cứ như thế đã, sau này khi bài toán phức tạp hơn thì gánh nặng công việc cũng sẽ tăng lên, về quản lý bộ nhớ, về việc đảm bảo an toàn thread cho hàm để Excel có thể sử dụng cho Multithreaded Recalculation, v.v...
    Viết C++ còn dễ hơn viết trên Delphi ... ông biết tại sao không riêng file XLCALL.H chuẩn của Ms chỉ copy vào là sử dụng với các thư viện c++ xử lý chuỗi tốt hơn trên Delphi cho nên tôi hay nói đâu đó viết c++ còn dễ hơn cả viết mã trên VBA là vậy vì nếu cũng như vậy VBA mãi mãi không bao giờ viết được còn C++ viết quá đơn giản

    tới hôm nay tôi biết nhiều ngôn ngữ thì cái nào Delphi tốt thì viết còn cái nào C++ Builder tốt thì viết và Go nữa nên quá OK xử lý mọi trường hợp

    Như hôm tôi viết ABI trên Delphi mãi không xong toàn lỗi và Crash khi chuyển qua thừ C++ thì tốt hơn hay hơn và xử lý nhiều vấn đề khác nữa
     

    Tận dụng tính năng Multithreaded Recalculation của Excel để cải thiện hiệu năng khi tính toán lại bảng tính​

    Tiếp nối bài #11, hàm PrintHelloWorld đã sẵn sàng và có thể sử dụng trên bảng tính của Excel. Nhưng mà có một nhược điểm thế này, giả sử người dùng sử dụng hàm từ A1 đến A9, sau đó vì một nhu cầu nào đó mà muốn tính toán lại (ví dụ như mở lại sổ làm việc hoặc khi người dùng nhấn phím F9 hoặc tổ hợp phím liên quan đến phím F9, v.v…) thì Excel sẽ thực hiện lần lượt như sau:
    • Excel tính toán công thức ở ô A1.
    • Tính toán xong ô A1 thì chuyển sang ô A2.
    • Xong ô A2 thì chuyển sang ô A3.
    • Thứ tự thực hiện cứ thế lập đi lập lại cho đến khi ô A9 được tính toán xong.
    Cũng may là hàm PrintHelloWorld khá đơn giản (chỉ trả về “Hello, World”) là xong nên mọi thứ diễn ra rất nhanh. Nhưng nếu thay hàm PrintHelloWorld bằng một hàm tính toán nặng và mất nhiều thời gian nào đó thì thời gian để Excel hoàn thành việc tính toán trên toàn bộ bảng tính sẽ diễn ra rất lâu mới xong. Cũng may là từ phiên bản Excel 2007, Microsoft bổ sung một tính năng tên là Multithreaded Recalculation (tạm dịch là tính toán lại công thức nhờ sử dụng đa luồng), tính năng này cho phép mỗi công việc tính toán lại các công thức trên bảng tính được thực hiện trên từng thread riêng biệt (lên đến 1024 thread), nhờ đó mà mỗi công thức trên bảng tính đều được thực hiện song song (như ở trên thì các công thức trong ô từ A1 đến A9 sẽ được thực hiện một cách song song) và giúp cải thiện đáng kể thời gian chờ để tính toán xong.

    Dĩ nhiên là động đến thread thì sẽ phát sinh nhiều vấn đề phức tạp, mà cái đau đầu nhất vẫn là làm thế nào để điều khiển các thread một cách hài hòa khi các thread đó sử dụng chung một vùng nhớ. Excel cũng không ngoại lệ, để một hàm được xem là an toàn thread để tính năng Multithreard Recalculation có thể phát huy được sức mạnh của mình, hàm đó phải đáp ứng những tiêu chí dưới đây (chi tiết xem bài viết này: Multithreaded recalculation in Excel | Microsoft Learn):

    1784347520102.png

    Quay trở lại hàm PrintHelloWorld, khi xét những quy tắc trên thì có thể thấy hàm đã vi phạm quy tắc số 4, cụ thể là hàm đã sử dụng từ khóa static khi khai báo biến sử dụng XLOPER12. Để tìm hiểu nguyên nhân vì sao việc sử dụng biến static khiến cho hàm không an toàn thread, chúng ta hãy xem một ví dụ dưới đây.

    Giả sử chúng ta có một hàm Foo đã đăng ký an toàn thread với Excel như sau:

    C++:
    extern "C" __declspec(dllexport) LPXLOPER12 WINAPI Foo(double a, double b) {
        static XLOPER12 xlRet;
        double dblRet = 0;
        //Tính toán cái gì đấy rồi trả về giá trị cho biến dblRet
    
        xlRet.xltype = xltypeNum;
        xlRet.val.num = dblRet;
        //Làm một số công việc nào đó, vd: dọn dẹp chẳng hạn
        return &xlRet;
    }

    Giả sử khi tính toán lại bảng tính, Excel sinh ra các thread tương ứng để tính toán những công thức nào có chứa hàm Foo. Vấn đề bắt đầu phát sinh từ việc biến xlRet được khai báo bằng từ khóa xlRet, nghĩa là vùng nhớ mà xlRet trỏ đến sẽ là vùng nhớ dùng chung của DLL cho bất cứ thread nào gọi đến các hàm trong DLL và vùng nhớ này sẽ luôn khả dụng kể cả khi đã thoát khỏi hàm Foo. Sự nghiêm trọng ở đây là do là vùng nhớ dùng chung nên các thread có thể tranh nhau ghi vào theo một thứ tự không thể nào đoán được. Lấy ví dụ:

    Giả sử Excel sinh ra hai thread để tính toán lại những công thức có sử dụng hàm Foo, tùy thuộc vào tham số truyền vào mà khiến cho khối lượng tính toán hàm có thể nặng hoặc nhẹ, từ đó thời gian hoàn thành công việc của các thread có thể khác nhau sau mỗi lần chạy:
    • Thread 1 và 2 bắt đầu tính toán hàm Foo.
    • Thread 1 tính toán xong và truyền kết quả 3 vào biến xlRet (xlRet.val.num = dblRet, sắp sửa đến dòng return &xlRet;.
    • Nhưng vì một lý do nào đó mà thread 2 đã đến xlRet.val.num = dblRet; trước, truyền kết quả 2 vào rồi đi xuống dòng return &xlRet và kết thúc hàm.
    • Thread 1 lúc này mới đi qua dòng return &xlRet và kết thúc hàm.
    Vì xlRet là biến dùng chung giữa các thread, cho nên hậu quả là kết quả tính toán của thread 1 đã bị ghi đè bởi kết quả tính toán của thread 2, rõ ràng là kết quả sai lệch, bởi vậy hàm Foo được xem là hàm không an toàn thread. Để giải quyết tình trạng này, chúng ta có hai cách:
    • Khai báo biến xlRet trên vùng nhớ heap. Hoặc,
    • Sử dụng Thread-Local Storage API của Microsoft.
    Hai cách trên đều đảm bảo mỗi thread khi gọi hàm Foo đều sẽ được cấp một vùng nhớ hoàn toàn riêng biệt, không chung chạ với ai, đảm bảo hàm được an toàn thread. Bài viết tiếp theo sẽ trình bày cách thức thực hiện hai phương pháp trên.
     
    Lần chỉnh sửa cuối:

    Khắc phục vấn đề còn tồn đọng: Giúp hàm trở nên hàm an toàn thread​

    Như cuối bài trước đã đề cập, hàm Foo không an toàn thread khi nó sử dụng biến static để trả kết quả về cho Excel, mà biến này được sử dụng chung giữa những thread gọi đến hàm Foo nên rất dễ xảy ra trình trạng kết quả tính toán của thread này bị ghi đè bởi kết quả tính toán của thread khác. Để khắc phục tính trạng này, chúng ta có hai cách:

    Cách thứ nhất: Khai báo biến chứa giá trị trả về cho Excel trên vùng nhớ heap​

    Theo cách này, mỗi thread được cấp một vùng nhớ riêng biệt cho nên không cần phải lo lắng việc thread này ghi đè vào dữ liệu của thread khác.

    C++:
    extern "C" __declspec(dllexport) LPXLOPER12 WINAPI Foo(double a, double b) {
        LPXLOPER xlRet = new XLOPER12;
        if (!xlRet) return NULL;
        double dblRet = 0;
        //Tính toán cái gì đấy rồi trả về giá trị cho biến dblRet
        xlRet->xltype = xltypeNum | xlbitDLLFree;
        xlRet->val.num = dblRet;
        //Làm một số công việc nào đó, vd: dọn dẹp chẳng hạn
        return xlRet;
    }

    Vì xlRet là vùng nhớ được cấp phát động, cho nên chúng ta cần bật cờ bit xlbitDLLFree để Excel gọi lại hàm xlAutoFree12 và từ đó giải phóng vùng nhớ mà biến này trỏ đến.

    C++:
    extern "C" __declspec(dllexport) int WINAPI xlAutoFree12(LPXLOPER12 pOper) {
        if (!pOper) return 0;
        delete pOper;
        return 1;
    }

    Cách thứ hai: Sử dụng TLS API​

    Cách triển khai này thì phức tạp hơn so với cách thứ nhất, lúc này chúng ta sử dụng TLS API để quản lý vùng nhớ được cấp cho các thread theo chỉ số. Khi Excel nạp XLL thì xin cấp phát chỉ số TLS để chứa thông tin về vùng nhớ dành riêng cho thread gọi đến DLL, từ lần đầu sử dụng thì cấp phát vùng nhớ để những lần sau dùng luôn vùng nhớ này, tránh việc xin cấp/giải phóng vùng nhớ nhiều lần không cần thiết. Khi Excel không còn sử dụng XLL nữa thì yêu cầu hệ điều hành thu hồi chỉ số TLS đã cấp cho các thread.

    C++:
    DWORD dwTlsIndex;
    
    BOOL APIENTRY DllMain( HMODULE hModule,
                           DWORD  ul_reason_for_call,
                           LPVOID lpReserved
                         )
    {
        switch (ul_reason_for_call)
        {
        case DLL_PROCESS_ATTACH:
        case DLL_THREAD_ATTACH:
        {
            if ((dwTlsIndex = TlsAlloc()) == TLS_OUT_OF_INDEXES) return FALSE;
            break;
        }
        case DLL_THREAD_DETACH:
        case DLL_PROCESS_DETACH:
            TlsFree(dwTlsIndex);
            dwTlsIndex = 0;
            break;
        }
        return TRUE;
    }

    Tiếp theo, gán vùng nhớ mà thread có thể sử dụng theo chỉ số TLS. Người dùng có thể gán một XLOPER12 theo chỉ số TLS là được, hoặc nếu có nhu cầu sử dụng thêm nhiều XLOPER12 thì có thể khai báo một struct nào đó rồi để các trường trong struct này mang kiểu XLOPER12 chẳng hạn, tất nhiên là tùy thuộc vào nhu cầu của người dùng.

    C++:
    typedef struct _TLSDATA {
        XLOPER12 xlOper1;
        XLOPER12 xlOper2;
    }TLSDATA, *LPTLSDATA;
    
    LPTLSDATA GetTlsData(DWORD dwTlsIndex) {
        LPTLSDATA pData = (LPTLSDATA)TlsGetValue(dwTlsIndex);
        if (!pData) {
            pData = new TLSDATA;
            if (!pData) return NULL;
            if (!TlsSetValue(pData)) {
                delete pData;
                return NULL;
            }
        }
        return pData;
    }

    Cuối cùng là sử dụng vùng nhớ đã cấp cho thread theo chỉ số TLS. Không cần phải giải phóng vùng nhớ này trong hàm xlAutoFree12, chỉ cần giải phóng vùng nhớ mà XLOPER12 có con trỏ trỏ đến vùng nhớ cấp phát động là được.

    C++:
    extern "C" __declspec(dllexport) LPXLOPER12 WINAPI Foo(double a, double b) {
        LPTLSDATA pTlsData = GetTlsData(dwTlsIndex);
        if (!pTlsData) return NULL;
        LPXLOPER xlRet = &pTlsData->xlOper1;
        double dblRet = 0;
        //Tính toán cái gì đấy rồi trả về giá trị cho biến dblRet
        xlRet->xltype = xltypeNum | xlbitDLLFree;
        xlRet->val.num = dblRet;
        //Làm một số công việc nào đó, vd: dọn dẹp chẳng hạn
        return xlRet;
    }

    Để biết thêm chi tiết về những khía cạnh giúp cho hàm trở nên an toàn thread, người dùng có thể tham khảo bài viết dưới đây của Microsoft:
    Multithreading and Memory Contention in Excel
     
    Lần chỉnh sửa cuối:
    Quay trở lại bài #6 có đề cập đến lý do vì sao phải sao chép sâu (deep copy) các tham số trước khi chuyển công việc tính toán sang thread nền trong hàm bất đồng bộ. Để hiểu được lý do vì sao phải làm như vậy, chúng ta hãy xem một ví dụ dưới đây.
    C++:
    typedef struct _PRINTINPUTARGS {
        LPXLOPER12 pxArgs;
        int cArgs;
    }PRINTINPUTARGS, *LPPRINTINPUTARGS;
    
    DWORD WINAPI PrintInputProc(LPVOID lpParam) {
        if (!lpParam) return 1;
        LPPRINTINPUTARGS pArgs = (LPPRINTINPUTARGS)lpParam;
        if (!pArgs->cArgs) return 1;
        Sleep(5000);
        Excel12(xlAsyncReturn, NULL, 2, &pArgs->pxArgs[1], &pArgs->pxArgs[0]);
        delete[] pArgs->pxArgs;
        return 0;
    }
    
    extern "C" __declspec(dllexport) void WINAPI PrintInput(LPXLOPER12 pxInput, LPXLOPER12 pxAsyncHandle) {
        XLOPER12 xlErr;
        xlErr.xltype = xltypeErr;
        xlErr.val.err = xlerrValue;
        if (!pxInput || !pxAsyncHandle || !(pxInput->xltype & xltypeStr) || !pxInput->val.str) {
            Excel12(xlAsyncReturn, NULL, 2, pxAsyncHandle, &xlErr);
            return;
        }
        PRINTINPUTARGS args;
        args.cArgs = 2;
        args.pxArgs = new XLOPER12[2];
        if (!args.pxArgs) {
            xlErr.val.err = xlerrNA;
            Excel12(xlAsyncReturn, NULL, 2, pxAsyncHandle, &xlErr);
            return;
        }
        args.pxArgs[0].xltype = pxInput->xltype;
        args.pxArgs[0].val = pxInput->val;
        args.pxArgs[1].xltype = pxAsyncHandle->xltype;
        args.pxArgs[1].val = pxAsyncHandle->val;
        DWORD dwThreadId;
        HANDLE hThread = CreateThread(NULL, 0, PrintInputProc, &args, 0, &dwThreadId);
        if (!hThread) {
            delete[] args.pxArgs;
            xlErr.val.err = xlerrNA;
            Excel12(xlAsyncReturn, NULL, 2, pxAsyncHandle, &xlErr);
        }
    }
    Hàm PrintInput là hàm được người dùng nhập vào ô trên bảng tính, nó có tác dụng trả về đúng như giá trị mà người dùng đã nhập vào pxInput, còn pxAsyncHandle là biến mà Excel dùng để theo dõi và nhận kết quả mà hàm bất đồng bộ trả về. Trước tiên hàm PrintInput chuẩn bị các tham số thông qua một struct tên là PRINTINPUTARGS, sao chép các tham số mà Excel gửi đến, sinh ra một thread rồi kết thúc hàm (hoặc trả về lỗi nếu cần thiết). Thread mà hàm PrintInput sinh ra có nhiệm vụ tính toán mà hàm này đã giao phó, thông qua pxAsyncHandle để trả kết quả về Excel, sau đó tiến hành dọn dẹp rồi kết thúc xử lý. Tuy nhiên khi chạy thử trong Excel thì phát sinh một vấn đề đó là Excel không phản hồi một lúc lâu rồi sau đó tự đóng. Nhìn sơ qua là biết đây chính là hành vi điển hình của lỗi Access Violation, tức là lỗi truy cập vào vùng nhớ nào đó không được phép, hệ điều hành tuýt còi và ép buộc Excel phải dừng lại. Vậy chắc chắn đoạn mã phía trên có vấn đề, thử kiểm tra xem sao.
    Sau khi xem xong thì có thể phát hiện thấy có một số vấn đề như sau:
    1. Biến args được cấp phát trên vùng nhớ stack (xếp chồng), truyền địa chỉ của biến vào thread, khi thread sử dụng thì lúc này hàm PrintInput đã kết thúc, vùng nhớ stack được cấp để chạy hàm này đã bị thu hồi, cho nên sẽ trả về lỗi Access Violation -> Excel bị sập.
    2. args.pxArgs[0] chỉ sao chép nông (shallow copy) giá trị của pxInput, tức là nó chỉ sao chép mỗi địa chỉ của chuỗi mà pxInput trỏ đến, và khi thread truy cập chuỗi thông qua địa chỉ này thì hàm PrintInput đã kết thúc và Excel đã giải phóng bộ nhớ cho chuỗi mà pxInput trỏ đến --> Excel bị sập giống như vấn đề số 1.
    Vậy thì nên giải quyết vấn đề này như thế nào? Như bài #6 đã chỉ ra, chỉ cần làm hai việc sau đây:
    1. Cấp phát biến args trên vùng nhớ heap. Và,
    2. Sao chép sâu chuỗi mà pxInput trỏ đến.
    Để sao chép sâu một XLOPER12 nào đó mang kiểu chuỗi thì có thể viết một hàm như dưới đây chẳng hạn:
    C++:
    BOOL DeepCopyXl12Str(const LPXLOPER12 pxInput, LPXLOPER12 pxOutput) {
        if (!pxInput || !pxOutput || !(pxInput->xltype & xltypeStr) || !pxInput->val.str) return FALSE;
        pxOutput->xltype = pxInput->xltype;
        wchar_t len = (wchar_t)pxInput->val.str[0];
        wchar_t* pwszRet = new wchar_t[len + 2];
        if (!pwszRet) {
            return FALSE;
        }
        pwszRet[0] = len;
        errno_t err = memcpy_s(pwszRet + 1, (rsize_t)(len + 1) * sizeof(wchar_t), pxInput->val.str + 1, len * sizeof(wchar_t));
        if (err) {
            delete[] pwszRet;
            return FALSE;
        }
        pwszRet[len + 1] = (wchar_t)'\0';
        pxOutput->val.str = pwszRet;
        return TRUE;
    }
    Sau đó sửa lại hàm PrintInput theo đúng hai yêu cầu ở trên là được.
    C++:
    DWORD WINAPI PrintInputProc(LPVOID lpParam) {
        if (!lpParam) return 1;
        LPPRINTINPUTARGS pArgs = (LPPRINTINPUTARGS)lpParam;
        if (!pArgs->cArgs) return 1;
        Sleep(5000);
        Excel12(xlAsyncReturn, NULL, 2, &pArgs->pxArgs[1], &pArgs->pxArgs[0]);
        delete[] pArgs->pxArgs[0].val.str;
        delete[] pArgs->pxArgs;
        delete pArgs;
        return 0;
    }
    
    extern "C" __declspec(dllexport) void WINAPI PrintInput(LPXLOPER12 pxInput, LPXLOPER12 pxAsyncHandle) {
        XLOPER12 xlErr;
        xlErr.xltype = xltypeErr;
        xlErr.val.err = xlerrValue;
        if (!pxInput || !pxAsyncHandle || !(pxInput->xltype & xltypeStr) || !pxInput->val.str) {
            return;
        }
        LPPRINTINPUTARGS args = new PRINTINPUTARGS;
        if (!args) {
            Excel12(xlAsyncReturn, NULL, 2, pxAsyncHandle, &xlErr);
            return;
        }
        args->cArgs = 2;
        args->pxArgs = new XLOPER12[2];
        if (!args->pxArgs) {
            xlErr.val.err = xlerrNA;
            Excel12(xlAsyncReturn, NULL, 2, pxAsyncHandle, &xlErr);
            return;
        }
        args->pxArgs[0].xltype = pxInput->xltype;
        if (!DeepCopyXl12Str(pxInput, &args->pxArgs[0])) {
            delete[] args->pxArgs;
            delete args;
            Excel12(xlAsyncReturn, NULL, 2, pxAsyncHandle, &xlErr);
            return;
        }
        args->pxArgs[1].xltype = pxAsyncHandle->xltype;
        args->pxArgs[1].val = pxAsyncHandle->val;
        DWORD dwThreadId;
        HANDLE hThread = CreateThread(NULL, 0, PrintInputProc, args, 0, &dwThreadId);
        if (!hThread) {
            delete[] args->pxArgs[0].val.str;
            delete[] args->pxArgs;
            delete args;
            xlErr.val.err = xlerrNA;
            Excel12(xlAsyncReturn, NULL, 2, pxAsyncHandle, &xlErr);
        }
    }
     
    Lần chỉnh sửa cuối:
    Tính chinh phục của Tôi là phải được từ bài số 18 tôi mới phát hiện ra sự thật 100/100

    1/ Khi viết C++ hay C++ builder thì điều là c++ cả chỉ khác IDE của Tools sử dụng nó có khác chút ít chỉ là do Tools đó họ chỉnh sửa lại ít cho phù hợp

    2/ Viết Visual C++ Hay C++ builder điều ăn sẵn cái có sẵn cái có sẵn của Ms là XLCALL.H mà không phải mất thời gian dịch lại sang .pas hay ngôn ngữ khác

    nên quá dễ không mất thời gian dịch gần 2000 dòng mã nữa và phát sinh đầy thứ sai sót khó kiểm soát

    3/ suy cho cùng viết các ứng dụng khác tôi không dám khẳng định còn viết xll cho Excel thì Viết C++ hay C++ builder là dễ viết nhất code ngắn nhất và xử lý hiệu năng tốt nhất

    4/ xong dựa vào XLCALL.H tự định nghĩa lấy thêm các hàm tuỳ chỉnh khác

    5/ Còn các chủ tái sử dụng : Excel-DNA , ADD-IN EXPRESS™ hay của bên thứ ba nào đó viết XLL có lẽ cũng ít quan tâm không đáng nhắc tới
     
    Tính chinh phục của Tôi là phải được từ bài số 18 tôi mới phát hiện ra sự thật 100/100

    1/ Khi viết C++ hay C++ builder thì điều là c++ cả chỉ khác IDE của Tools sử dụng nó có khác chút ít chỉ là do Tools đó họ chỉnh sửa lại ít cho phù hợp

    2/ Viết Visual C++ Hay C++ builder điều ăn sẵn cái có sẵn cái có sẵn của Ms là XLCALL.H mà không phải mất thời gian dịch lại sang .pas hay ngôn ngữ khác

    nên quá dễ không mất thời gian dịch gần 2000 dòng mã nữa và phát sinh đầy thứ sai sót khó kiểm soát

    3/ suy cho cùng viết các ứng dụng khác tôi không dám khẳng định còn viết xll cho Excel thì Viết C++ hay C++ builder là dễ viết nhất code ngắn nhất và xử lý hiệu năng tốt nhất

    4/ xong dựa vào XLCALL.H tự định nghĩa lấy thêm các hàm tuỳ chỉnh khác

    5/ Còn các chủ tái sử dụng : Excel-DNA , ADD-IN EXPRESS™ hay của bên thứ ba nào đó viết XLL có lẽ cũng ít quan tâm không đáng nhắc tới
    Dễ kiểu gì, trong bài này mới chỉ hello world thôi mà đã có lắm thứ phải lo giải quyết, trong thực tế gặp nhiều tình huống, bài toán phức tạp hơn nhiều thì khối lượng công việc sẽ lớn lắm.
     
    Dễ kiểu gì, trong bài này mới chỉ hello world thôi mà đã có lắm thứ phải lo giải quyết, trong thực tế gặp nhiều tình huống, bài toán phức tạp hơn nhiều thì khối lượng công việc sẽ lớn lắm.
    viết rồi tôi mới nói chứ chưa viết thì có lẽ không bao giờ nói cả

    1/ Link sau viết XLL thuần Delphi phải dịch XLCALL.H sang XLCALL.pas gần 2000 dòng mã + lỗi tiền ẩn khó đoán sau này

    2/ Khi viết lại chức năng link trên thì cái dễ nhất là không phí thời gian dịch lại gần 2000 dòng mã XLCALL.H nữa mà chỉ copy và sử dụng

    3/ cùng chức năng như nhau viết lại trên C++ giảm đi khoãng 2000 dòng mã so với viết trên Delphi ( không tính XLCALL.H )

    xử lý mọi cái tốt hơn và sau này Ms có ra bộ SDK mới cho XLL thì tải về copy XLCALL.H vào vào sử dụng không mất thời gian dịch lại nữa


    Kết luận
    Qua hai lý do trên chứng minh một điều nếu ai muốn viết XLL cho Excel thì nên viết trên C++ hay C++ Buider cũng như nhau cả và dễ viết nhất

    sau này Ms nâng cấp các bản mới thì chỉ tải File về thay thế là xong trừ khi cấu trúc Class hay hàm có thay đổi ( nhưng ít lắm )
     
    (Giải trí) Tối ưu tốc độ bằng Hợp ngữ (Assembly)
    Dự án hiện tại mã viết bằng C++ đã chạy nhanh và rất ổn định, nhưng người dùng ám ảnh với hai chữ "tốc độ" lại cảm thấy cần phải nhanh hơn nữa. Đến lúc này người dùng có thể cân nhắc phương án dùng Hợp ngữ. Hợp ngữ là ngôn ngữ lập trình cấp thấp gần với ngôn ngữ máy nhất, cho tốc độ nhanh nhất so với những ngôn ngữ lập trình bậc cao khác như C (được mệnh danh là Hợp ngữ phiên bản bậc cao), C++, C# và Java, v.v... Tất nhiên cái giá phải trả là nắm được rất nhiều kiến thức về CPU, về thanh ghi (register), chỉ lệnh (instruction) và quản lý bộ nhớ, lập trình bằng Hợp ngữ cũng rất vất vả, mệt nhọc và tốn thời gian.
    Lấy ví dụ trong bài viết này đang có sẵn hàm PrintHelloWorld, giả sử người dùng cảm thấy hàm chuyển đổi dưới đây vẫn chưa cho tốc độ ưng ý:
    C++:
    wchar_t* WideCharToXl12Str1(const wchar_t* lpwszInput) {
        size_t len;
        if (!lpwszInput || !(len = wcslen(lpwszInput))) return NULL;
        if (len > MAX_XL12_STR_LEN) len = MAX_XL12_STR_LEN;
        wchar_t* pwszResult = new wchar_t[len + 2];
        if (!pwszResult) return NULL;
        pwszResult[0] = (wchar_t)len;
        errno_t err = memcpy_s(pwszResult + 1, (len + 1) * sizeof(wchar_t), lpwszInput, len * sizeof(wchar_t));
        if (err) {
            delete[] pwszResult;
            return NULL;
        }
        pwszResult[len + 1] = '\0';
        return pwszResult;
    }
    Người dùng bèn bắt tay viết lại hàm trên bằng Hợp ngữ, trong trường hợp bài viết này đang sử dụng Excel phiên bản 64 bit cho nên mã viết bằng Hợp ngữ cũng sẽ tuân thủ những yêu cầu mà phiên bản 64 bit đưa ra. Mã được viết theo cú pháp NASM, người dùng có thể tìm hiểu thêm về trình biên dịch này tại địa chỉ NASM — The Netwide Assembler.
    Mã:
    NULL equ 0
    MAX_XL12_STR_LEN equ 32767
    
    extern HeapAlloc
    extern HeapFree
    extern GetProcessHeap
    
    global DllMain
    global LengthOfWideChar
    global WideCharToXl12Str
    
    section .text
    
    ;wchar_t* WideCharToXl12Str(const wchar_t* lpInput);
    WideCharToXl12Str:
        test rcx, rcx ; kiểm tra xem lpInput có bị NULL không
        jz .return_NULL ; nếu có thì thoát khỏi hàm
        push rbp ; lưu lại base pointer (con trỏ cơ sở)
        mov rbp, rsp ; gán stack pointer (con trỏ trỏ đến bộ nhớ xếp chồng) cho base pointer
        sub rsp, 40 ; để dành 32 byte làm shadow space + 8 byte để đảm bảo stack alignment (căn chỉnh bộ nhớ xếp chồng)            
        mov qword [rbp-24], rcx ; lưu lpInput vào stack
        call LengthOfWideChar ; gọi hàm để lấy độ dài của chuỗi lpInput
        test rax, rax ; kiểm tra xem lpInput có phải là chuỗi rỗng hay không
        jz .return_NULL ; nếu đúng thì thoát khỏi hàm
        mov qword [rbp-8], rax ; gán kết quả của hàm LengthOfWideChar vào stack
        cmp qword [rbp-8], MAX_XL12_STR_LEN ; nếu số lượng ký tự của lpInput vượt mức tối đa
        jg .set_max_str_len ; thì thiết lập theo mức tối đa
    .continue:
        call GetProcessHeap ; Lấy điều khiển vùng nhớ heap của tiến trình, tham số ở thanh ghi rcx
        mov qword [rbp-16], rax ; gán kết quả của hàm GetProcessHeap vào stack
        mov rax, qword [rbp-8] ; chuẩn bị tính toán số byte cần cấp phát động
        shl rax, 1 ; 2 byte * số lượng ký tự                
        add rax, 4 ; +2 byte để chứa thông tin độ dài chuỗi và ký tự ngắt chuỗi              
        mov r8, rax ; gán vào tham số thứ 3
        mov rcx, qword [rbp-16] ; gán kết quả của hàm GetProcessHeap vào tham số thứ 1
        xor rdx, rdx ; tham số thứ hai là 0
        call HeapAlloc ; gọi hàm để xin cấp phát động vùng nhớ. GetProcessHeap(rcx, rdx, r8)
        test rax, rax ; kiểm tra con trỏ mà hàm HeapAlloc trả về
        jz .return_NULL ; nếu là NULL thì thoát khỏi hàm
        mov rdi, rax ; gán vùng nhớ vừa được cấp phát động vào thanh ghi rdi
        mov rcx, qword [rbp-8] ; Số lượng ký tự cần sao chép từ lpInput vào thanh ghi rcx
        mov word [rdi], cx ; phần tử đầu tiên chứa thông tin về số lượng ký tự
        add rdi, 2 ; chuyển đến vị trí thứ hai để sẵn sàng sao chép
        mov rsi, qword [rbp-24] ; gán lpInput vào thanh ghi rsi
    .copy: ; bắt đầu sao chép
        mov rdx, qword [rsi] ; gán giá trị của phần tử hiện tại của lpInput vào thanh ghi rdx
        mov word [rdi], dx ; đổi kiểu xuống còn 2 byte
        add rsi, 2 ; chuyển vị trí đọc lpInput lên vị trí tiếp theo
        add rdi, 2 ; chuyển vị trí cần ghi vào của vùng nhớ được cấp phát động lên vị trí tiếp theo
        dec rcx ; len -=1 lùi lại 1 đơn vị. len = số lượng ký tự cần sao chép từ lpInput
        cmp rcx, 0 ; nếu len = 0 thì thoát khỏi vòng lặp
        jnz .copy ; tiếp tục vòng lặp khi len > 0
    .copy_done:
        mov word [rdi], 0 ; gán ký tự ngắt chuỗi vào cuối vùng nhớ được cấp phát động
        leave ; thiết lập lại stack pointer
        ret ; thoát khỏi hàm, thanh ghi rax đã chứa con trỏ trỏ đến vùng nhớ được cấp phát động
    .set_max_str_len:
        mov qword [rbp-8], MAX_XL12_STR_LEN ; trường hợp số lượng ký tự cần sao chép vượt quá mức tối đa thì thiết lập theo mức tối đa
        jmp .continue ; tiếp tục thực thi mã theo nhãn .continue
    .return_NULL: ; xử lý trường hợp khi thực thi không thành công
        xor rax, rax ; trả về NULL
        leave ; thiết lập lại stack pointer
        ret ; thoát khỏi hàm
    
    ;wchar_t* LengthOfWideChar(const wchar_t* lpInput);
    LengthOfWideChar:          
        mov rsi, rcx ; gán lpInput vào thanh ghi rsi          
        cmp rsi, NULL ; kiểm tra xem lpInput có bị NULL không
        je .return_NULL ; nếu có thì trả về NULL
        xor rcx, rcx ; thiết lập thanh ghi rcx về 0, dùng để đếm số lượng ký tự          
    .loop:
        mov ax, [rsi] ; đọc 2 byte từ lpInput        
        test ax, ax ; kiểm tra xem 2 byte vừa đọc có phải là ký tự ngắt chuỗi ('\0') không
        jz .exit_loop ; nếu có thì thoát khỏi vòng lặp
        inc rcx ; 2 byte vừa đọc không phải là ký tự '\0', cho nên +1 ký tự
        add rsi, 2 ; chuyển vị trí đọc sang 2 byte tiếp theo
        jmp .loop ; tiếp tục vòng lặp
    .exit_loop:
        mov rax, rcx ; gán kết quả để chuẩn bị thoát khỏi hàm          
        ret
    .return_NULL:
        xor rax, rax ; trả về NULL          
        ret
    Dùng trình biên dịch NASM, người dùng biên dịch ra tập tin .obj.
    1786196130343.png
    1786196160677.png
    Sau đó nhúng tập tin .obj vào dự án. Tùy thuộc vào trình soạn thảo mã (IDE) mà sẽ có nhiều cách triển khai khác nhau, trong bài viết này sử dụng Visual Studio, kéo thả vào Solution Explorer là xong.
    1786196262285.png
    Sau đó khai báo hàm như dưới đây là được.
    C++:
    extern "C" wchar_t* WINAPI WideCharToXl12Str(const wchar_t* lpwszInput);
    Cuối cùng là biên dịch ra DLL rồi đổi thành .XLL là xong.
    P/s: Lưu ý mã trong bài viết này chỉ dùng để trình diễn mà thôi, trong thực tế tốc độ chênh lệch giữa hai phiên bản C/C++ và Hợp ngữ có thể ít hoặc nhiều, tùy thuộc vào bài toán cần giải quyết. Thường thì người ta sẽ dùng Hợp ngữ khi:
    • Trình biên dịch không thể tận dụng được những chỉ lệnh đặc thù nào đó, và
    • Khi cần tối ưu khắt khe, nhất là đối với vòng lặp.
    • ...
     
    Lần chỉnh sửa cuối:

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

    Back
    Top Bottom