Một ví dụ về XLL add-in: Tương tác với Google Gemini dưới dạng công thức bảng tính (5 người xem)

Liên hệ QC

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
    157
    Được thích
    146
    Tiếp nối bài viết Thử lập trình XLL add-in bằng C/C++, nay tôi xin giới thiệu một ví dụ về việc sử dụng XLL add-in để tương tác với Google Gemini dưới dạng công thức bảng tính.
    Google Gemini là một chat bot do Google phát triển nhằm chạy đua với ChatGPT của OpenAI, Microsoft Copilot của Microsoft và nhiều hãng công nghệ khác. Khởi đầu với cái tên Google Bard, nay nó đã phát triển vượt bậc với cái tên Gemini và tỏ ra không hề kém cạnh so với các sản phẩm chat bot đình đám hiện nay.
    Tương tự như nhiều sản phẩm khác, Google cho phép tích hợp chat bot này vào các ứng dụng khác nhau dưới dạng API, dưới dạng gói dịch vụ miễn phí và trả phí. Để biết thêm thông tin chi tiết, người dùng có thể tham khảo bài viết này: Google AI Studio quickstart.
    Sau khi đã tạo tài khoản và/hoặc đăng nhập, người dùng có thể lấy khóa API tại đây: Quickly test the Gemini API
    Dưới đây là toàn bộ code về XLL add-in trong bài viết này, mấu chốt nằm ở class Gemini khi nó đảm nhận các công việc then chốt như tạo, gửi yêu cầu, xử lý kết quả trả về cho Excel hiển thị. Code đều được chú thích cụ thể, rõ ràng để mọi người có thể dễ theo dõi và tránh cho bài viết này dài dòng và lan man.
    Lưu ý: Khóa API được gán trực tiếp vào code, điều này có thể không an toàn do người có chuyên môn có thể soi thấy khóa API, cho nên cách tốt nhất là người dùng nên sửa lại code cho phép lấy thông tin khóa API từ tập tin bên ngoài.

    Gemini.h:

    C++:
    #pragma once
    #import <winhttpcom.dll>
    #include <comdef.h>
    #include <comutil.h>
    #include "nlohmann/json.hpp"
    #include <vector>
    #include "Response.h"
    
    class Gemini
    {
    public:
        Gemini(PCWSTR lpszKey);
        virtual HRESULT AskGemini(PCWSTR szPrompt, Response& response, WCHAR** lppszError);
        BOOL ApiKey(PCWSTR lpszValue);
        PCWSTR GetApiKey() const;
    private:
        PCWSTR lpszApiKey = L"";
    };

    Gemini.cpp:
    C++:
    #include "Gemini.h"
    
    Gemini::Gemini(PCWSTR lpszKey) {
        size_t len;
        if (!lpszKey || !(len = wcslen(lpszKey))) lpszApiKey = L"";
        else lpszApiKey = lpszKey;
    }
    
    BOOL Gemini::ApiKey(PCWSTR lpszValue) {
        if (!lpszValue) return FALSE;
        lpszApiKey = lpszValue;
        return lpszApiKey ? TRUE : FALSE;
    }
    
    PCWSTR Gemini::GetApiKey() const {
        return lpszApiKey;
    }
    
    /*
        Hàm này chuyển đổi kiểu chuỗi BSTR thành kiểu chuỗi WCHAR (hay wchar_t)
        BSTR hay WCHAR thực chất đều giống nhau,
        chỉ khác ở chỗ BSTR quy định 4 byte đầu tiên chứa thông tin về độ dài chuỗi
        Tham khảo thêm: https://learn.microsoft.com/en-us/previous-versions/windows/desktop/automat/bstr
    */
    static WCHAR* BstrToWchar(BSTR bstrVal) {
        size_t len;
        //Nếu biến bstrVal là con trỏ không hợp lệ (0) hoặc độ dài chuỗi là 0 thì trả về NULL (0)
        if (!bstrVal || !(len = SysStringLen(bstrVal))) return NULL;
        //Cấp phát bộ nhớ động cho chuỗi kết quả, chừa ra 2 byte để chứa ký tự rỗng kết thúc
        wchar_t* wstrRetVal = new wchar_t[len + 1];
        //Nếu không thể cấp phát bộ nhớ tức là hết bộ nhớ thì trả về NULL
        if (!wstrRetVal) return NULL;
        //Sao chép nội dung mà bstrVal trỏ đến vào biến wstrRetVal
        memcpy(wstrRetVal, bstrVal, len * sizeof(WCHAR));
        //Gán ký tự tự rỗng kết thúc vào vị trí cuối cùng của mảng chuỗi, sau đó trả về kết qủa
        wstrRetVal[len] = '\0';
        return wstrRetVal;
    }
    
    /*
        Hàm này chuyển đổi kiểu chuỗi BSTR thành chuỗi mã hóa (encode) UTF-8
        Để các hàm xử lý chuỗi JSON có thể làm việc
    */
    static char* BstrToChar(BSTR bstrVal) {
        size_t len;
        //Nếu biến bstrVal là con trỏ không hợp lệ (0) hoặc độ dài chuỗi là 0 thì trả về NULL (0)
        if (!bstrVal || !(len = SysStringLen(bstrVal))) return NULL;
        //Xác định độ dài chuỗi cần thiết để chuẩn bị cho việc cấp phát bộ nhớ động
        int strLen = WideCharToMultiByte(CP_UTF8, 0, bstrVal, (int)len, NULL, 0, NULL, NULL);
        //Tiến hành cấp phát bộ nhớ động, chừa ra một byte để đánh dấu ký tự rỗng kết thúc
        char* strRetVal = new char[strLen + 1];
        //Nếu không thể cấp phát bộ nhớ tức là hết bộ nhớ thì trả về NULL
        if (!strRetVal) return NULL;
        //Trường hợp hàm WideCharToMultiByte trả về 0, nghĩa là quá trình chuyển đổi không thành công
        if (!WideCharToMultiByte(CP_UTF8, 0, bstrVal, (int)len, strRetVal, strLen, NULL, NULL)) {
            //Giải phóng bộ nhớ động đã cấp phát, sau đó thoát khỏi hàm
            delete[] strRetVal;
            return NULL;
        }
        //Gán ký tự tự rỗng kết thúc vào vị trí cuối cùng của mảng chuỗi, sau đó trả về kết quả
        strRetVal[strLen] = '\0';
        return strRetVal;
    }
    
    /*
        Hàm này chuyển đổi ký tự mã hóa UTF-8 thành mảng byte (byte array)
    */
    static HRESULT CharToSafeArray(LPCSTR strVal, SAFEARRAY** ppsa) {
        //Nếu strVal là con trỏ không hợp lệ thì thoát khỏi hàm
        if (!strVal) return E_POINTER;
        //Xác định độ dài chuỗi
        size_t len = strlen(strVal);
        //Nếu là chuỗi rỗng (hay "") thì thoát khỏi hàm
        if (!len) return E_INVALIDARG;
        HRESULT hr;
        //Tiến hành khai báo kích thước mảng, cận dưới là 0 và số lượng phần tử tương ứng với độ dài chuỗi
        SAFEARRAYBOUND sab[1]{};
        sab[0].cElements = (ULONG)len;
        sab[0].lLbound = 0;
        /*
            Khai báo mảng SAFEARRAY
            Để biểu diễn một dãy byte thì chỉ cần kiểu số không âm VT_UI1 (hay ushort) là đủ
        */
        SAFEARRAY* psa = SafeArrayCreate(VT_UI1, 1, sab);
        //Nếu không thể khởi tạo mảng thì nghĩa là đã hết bộ nhớ
        if (!psa) return E_OUTOFMEMORY;
        //Khai báo biến lpData để truy cập mảng
        LPVOID lpData = NULL;
        //Tiến hành khóa và truy cập mảng
        hr = SafeArrayAccessData(psa, &lpData);
        //Nếu không thể khóa truy cập mảng thì tiến hành dọn dẹp bộ nhớ và thoát khỏi hàm
        if (FAILED(hr)) {
            SafeArrayDestroy(psa);
            return hr;
        }
        //Sao chép nội dung của biến strVal vào mảng
        memcpy(lpData, strVal, len);
        //Mở khóa truy cập mảng
        hr = SafeArrayUnaccessData(psa);
        //Nếu không thể mở khóa truy cập mảng thì tiến hành dọn dẹp bộ nhớ và thoát khỏi hàm
        if (FAILED(hr)) {
            SafeArrayDestroy(psa);
            return hr;
        }
        //Gán con trỏ kết quả
        *ppsa = psa;
        //Thông báo rằng hoạt động đã diễn ra thành công
        return S_OK;
    }
    //Các hàm ParsePart, ParseContent, ParseCandidate và ParseResponse chịu trách nhiệm xử lý chuỗi JSON
    static HRESULT ParsePart(const nlohmann::json& json, Part& part) {
        try {
            if (json.empty()) return E_INVALIDARG;
            if (json.contains("text") && !json.at("text").empty() && !json.at("text").is_null() && json.at("text").is_string())
                json.at("text").get_to<std::string>(part.Text);
        }
        catch (const nlohmann::json::type_error&) {
            return E_FAIL;
        }
        return S_OK;
    }
    
    static HRESULT ParseContent(const nlohmann::json& json, Content& result) {
        try {
            if (json.empty()) return E_INVALIDARG;
            if (json.contains("role") && !json.at("role").empty() && !json.at("role").is_null() && json.at("role").is_string())
                json.at("role").get_to<std::string>(result.Role);
            if (json.contains("parts") && !json.at("parts").empty() && !json.at("parts").is_null() && json.at("parts").is_array()) {
                std::vector<Part>parts;
                for (auto i = json["parts"].rbegin(); i < json["parts"].rend(); i++) {
                    nlohmann::json item = *i;
                    Part part{};
                    HRESULT hr = ParsePart(item, part);
                    if (FAILED(hr)) return hr;
                    parts.push_back(part);
                }
                result.Parts = parts;
            }
        }
        catch (const nlohmann::json::type_error&) {
            return E_FAIL;
        }
        return S_OK;
    }
    
    static HRESULT ParseCandidate(const nlohmann::json& json, Candidate& result) {
        try {
            if (json.empty()) return E_INVALIDARG;
            if (json.contains("finishReason") && !json.at("finishReason").empty() && !json.at("finishReason").is_null() && json.at("finishReason").is_string())
                json.at("finishReason").get_to<std::string>(result.FinishReason);
            if (json.contains("avgLogprobs") && !json.at("avgLogprobs").empty() && !json.at("avgLogprobs").is_null() && json.at("avgLogprobs").is_number_float())
                json.at("avgLogprobs").get_to<float>(result.AvgLogprobs);
            if (json.contains("content") && !json.at("content").empty() && !json.at("content").is_null() && !json.at("content").is_array()) {
                Content content{};
                nlohmann::json e = json["content"];
                HRESULT hr = ParseContent(e, content);
                if (FAILED(hr)) return hr;
                result.Content = content;
            }
        }
        catch (const nlohmann::json::type_error&) {
            return E_FAIL;
        }
        return S_OK;
    }
    
    static HRESULT ParseResponse(BSTR bstrResponse, Response& result) {
        if (!bstrResponse) return E_POINTER;
        size_t len = SysStringLen(bstrResponse);
        if (!len) return E_INVALIDARG;
        char* strResponse = BstrToChar(bstrResponse);
        if (!strResponse) return E_OUTOFMEMORY;
        Response response{};
        try {
            nlohmann::json json = nlohmann::json::parse(strResponse);
            delete[] strResponse;
            if (json.contains("candidates") && !json.at("candidates").is_null() && json.at("candidates").is_array()) {
                std::vector<Candidate>candidates;
                for (auto i = json["candidates"].rbegin(); i < json["candidates"].rend(); i++) {
                    nlohmann::json item = *i;
                    Candidate candidate{};
                    HRESULT hr = ParseCandidate(item, candidate);
                    if (FAILED(hr)) return hr;
                    candidates.push_back(candidate);
                }
                result.Candidates = candidates;
            }
            if (json.contains("modelVersion") && !json.at("modelVersion").is_null() && json.at("modelVersion").is_string())
                json.at("modelVersion").get_to<std::string>(result.ModelVersion);
        }
        catch (const nlohmann::json::type_error&) {
            delete[] strResponse;
            return E_FAIL;
        }
        return S_OK;
    }
    
    /*
        Hàm này có nhiệm vụ chính sau:
        1. Chuẩn bị chuỗi JSON để gửi đi thông qua thư viện WinHTTP COM
        2. Xử lý chuỗi JSON nhận được và trả kết quả
    */
    HRESULT Gemini::AskGemini(PCWSTR lpszPrompt, Response& response, WCHAR** lppszError) {
        using namespace WinHttp;
        size_t nPromptLen, nApiKeyLen;
        //Kiểm tra các con trỏ đầu vào phải đạt yêu cầu, nếu không phải thoát khỏi hàm
        if (!lpszPrompt || (nPromptLen = lstrlen(lpszPrompt)) == 0 || !lpszApiKey || (nApiKeyLen = lstrlen(lpszApiKey)) == 0) return E_POINTER;
        /*
            Xác định độ dài chuỗi cần thiết để chuẩn bị cấp phát bộ nhớ động
            Lý do sử dụng hàm WideCharToMultiByte là vì
            Thư viện nlohmann chỉ hỗ trợ xử lý chuỗi JSON dưới dạng mã hóa UTF-8
        */
        int nPromptNumChars = WideCharToMultiByte(CP_UTF8, 0, lpszPrompt, (int)nPromptLen, NULL, 0, NULL, NULL);
        //Cấp phát bộ nhớ động
        char* szUtf8Prompt = new char[nPromptNumChars + 1];
        //Nếu không thể cấp phát bộ nhớ tức là đã hết bộ nhớ, cần thoát khỏi hàm
        if (!szUtf8Prompt) return E_OUTOFMEMORY;
        //Tiến hành chuyển đổi chuỗi sang dạng mã hóa UTF-8, nếu không thành công thì dọn dẹp bộ nhớ và thoát khỏi hàm
        if (WideCharToMultiByte(CP_UTF8, 0, lpszPrompt, (int)nPromptLen, szUtf8Prompt, nPromptNumChars, NULL, NULL) == 0) {
            delete[] szUtf8Prompt;
            return E_FAIL;
        }
        //Gán ký tự rỗng kết thúc vào phần tử cuối cùng của mảng chuỗi
        szUtf8Prompt[nPromptNumChars] = '\0';
        try {
            //Khối lệnh này chịu trách nhiệm khởi tạo đối tượng JSON để gửi yêu cầu đến Google Gemini
            nlohmann::json jsonPart;
            jsonPart["text"] = szUtf8Prompt;
            delete[] szUtf8Prompt;
            nlohmann::json jsonParts = nlohmann::json::array({ jsonPart });
            nlohmann::json jsonContent;
            jsonContent["parts"] = jsonParts;
            nlohmann::json jsonContents = nlohmann::json::array({ jsonContent });
            nlohmann::json json;
            json["contents"] = jsonContents;
            //Khởi tạo thư viện COM
            HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE);
            if (FAILED(hr)) return hr;
            //Khởi tạo đối tượng dùng để gửi yêu cầu đến Google Gemini
            IWinHttpRequestPtr pWinHttp;
            hr = pWinHttp.CreateInstance(__uuidof(WinHttpRequest));
            //Nếu không thể khởi tạo đối tượng thì thoát khỏi hàm
            if (FAILED(hr)) return hr;
            /*
                Chuẩn bị URL để kết nối với Google Gemini
                Chuẩn bị các tham số cần thiết
            */
            _bstr_t bstrUrl("https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=" + _bstr_t(lpszApiKey));
            hr = pWinHttp->Open(_bstr_t("POST"), bstrUrl, VARIANT_TRUE);
            if (FAILED(hr)) {
                CoUninitialize();
                return hr;
            }
            hr = pWinHttp->SetRequestHeader(_bstr_t("Content-Type"), _bstr_t("application/json"));
            if (FAILED(hr)) {
                CoUninitialize();
                return hr;
            }
            //Chuyển đối tượng JSON thành dạng chuỗi
            std::string strJson(json.dump());
            //Khởi tạo mảng SAFEARRAY chứa chuỗi JSON ở dạng mảng byte
            SAFEARRAY* psa = NULL;
            hr = CharToSafeArray(strJson.c_str(), &psa);
            if (FAILED(hr)) {
                CoUninitialize();
                return hr;
            }
            VARIANT vtRequestBody{};
            VariantInit(&vtRequestBody);
            vtRequestBody.vt = VT_ARRAY | VT_UI1;
            vtRequestBody.parray = psa;
            //Gửi yêu cầu đi, nếu hoạt động không thành công thì dọn dẹp bộ nhớ và thoát khỏi hàm
            hr = pWinHttp->Send(vtRequestBody);
            if (FAILED(hr)) {
                CoUninitialize();
                VariantClear(&vtRequestBody);
                return hr;
            }
            //Dọn dẹp bộ nhớ sau khi dùng xong
            VariantClear(&vtRequestBody);
            /*
                Nếu quá thời gian 30 giây mà vẫn chưa nhận được phản hồi từ Google Gemini
                Xuất ra chuỗi thông báo, sau đó dọn dẹp bộ nhớ và thoát khỏi hàm
            */
            if (!pWinHttp->WaitForResponse(30)) {
                WCHAR lpszRequestTimedOut[] = L"Error: Request timed out";
                size_t len = wcslen(lpszRequestTimedOut);
                WCHAR* lpszText = new WCHAR[len + 1];
                if (!lpszText) return NULL;
                memcpy(lpszText, lpszRequestTimedOut, len * sizeof(WCHAR));
                lpszText[len] = '\0';
                *lppszError = lpszText;
                CoUninitialize();
                return E_FAIL;
            }
            //Kiểm tra status code trả về
            long nStatus = pWinHttp->GetStatus();
            //Nếu trả về mã 200 tức là yêu cầu đã thành công
            if (nStatus == 200) {
                //Lưu trữ kết quả dạng chuỗi
                _bstr_t bstrResponse(pWinHttp->GetResponseText());
                //Gọi hàm chuyển đổi (deserialize), hay giải tuần tự hóa, đối tượng JSON thành dạng struct
                hr = ParseResponse(bstrResponse, response);
                if (FAILED(hr)) {
                    CoUninitialize();
                    return E_FAIL;
                }
            }
            //Trường hợp trả về mã khác
            else {
                //Xuất ra chuỗi thông báo lỗi, dọn dẹp bộ nhớ và thoát khỏi hàm
                _bstr_t bstrError(pWinHttp->GetResponseText());
                WCHAR* wstrError = BstrToWchar(bstrError);
                *lppszError = wstrError;
                CoUninitialize();
                return E_FAIL;
            }
            CoUninitialize();
        }
        //Xử lý ngoại lệ không thể xử lý chuỗi thành đối tượng JSON
        catch (const std::exception&) {
            return E_FAIL;
        }
        //Thông báo rằng hoạt động đã diễn ra thành công
        return S_OK;
    }

    dllmain.cpp:

    C++:
    #include "XLCALL.H"
    #include "FRAMEWRK.H"
    #include "string"
    #include "Gemini.h"
    #include "framework.h"
    
    #pragma comment(lib, "XLCALL32.LIB")
    
    /*
        Khai báo biến lưu trữ chỉ mục giúp phân biệt vùng nhớ cục bộ cho mỗi thread
        Điều này cần thiết khi Excel áp dụng tính năng multithreaded recalculation, multithread calculation
        Tức là sử dụng đa luồng để tính toán các công thức trong bảng tính
        Lúc này mỗi thread sẽ được cấp phát động một vùng nhớ riêng biệt
        Giúp đảm bảo tính an toàn thread (thread safety)
    */
    DWORD dwTlsIndex;
    
    //Khai báo cấu trúc dữ liệu cần dùng
    struct TlsData {
        XLOPER12 data;
        XLOPER12 data2;
    };
    
    //Hàm này thực hiện việc cấp phát, trả về con trỏ trỏ đến vùng nhớ theo chỉ mục được gán cho mỗi thread
    TlsData* GetTlsData(void) {
        //Khởi tạo và xác định vùng nhớ đã được cấp phát
        TlsData* pTlsData = reinterpret_cast<TlsData*>(TlsGetValue(dwTlsIndex));
        //Nếu chưa được cấp phát bộ nhớ thì tiến hành cấp phát động bộ nhớ
        if (!pTlsData) {
            //Tiến hành cấp phát động bộ nhớ
            pTlsData = new TlsData[1];
            //Trường hợp hết bộ nhớ thì thoát khỏi hàm
            if (!pTlsData) return NULL;
            //Ngược lại, gán vùng nhớ được cấp phát động theo chỉ mục
            TlsSetValue(dwTlsIndex, pTlsData);
        }
        //Trả về con trỏ trỏ đến vùng nhớ
        return pTlsData;
    }
    
    //Hàm này khởi tạo thread-local storage, hay vùng nhớ cục bộ của mỗi thread
    BOOL InitializeTls(DWORD DllMainCallReason) {
        //Tùy thuộc vào trạng thái khi Excel sử dụng/ngừng sử dụng DLL
        switch (DllMainCallReason) {
        case DLL_PROCESS_ATTACH:
            //Khi Excel sử dụng DLL thì tiến hành cấp chỉ số cho thread
            if ((dwTlsIndex = TlsAlloc()) == TLS_OUT_OF_INDEXES)
                return FALSE;
            break;
            //Khi Excel ngừng sử dụng DLL thì tiến hành dọn dẹp vùng nhớ đã cấp cho thread theo chỉ mục
        case DLL_PROCESS_DETACH:
            TlsFree(dwTlsIndex);
            break;
        }
        return TRUE;
    }
    
    BOOL APIENTRY DllMain( HMODULE hModule,
                           DWORD  ul_reason_for_call,
                           LPVOID lpReserved
                         )
    {
        return InitializeTls(ul_reason_for_call);
    }
    
    /*
        Hàm này chuyển đổi chuỗi wide string thành dạng wide string mà Excel chấp nhận
        Dạng chuỗi wide string mà Excel chấp nhận cơ bản không khác gì mấy so với dạng wide string thông thường
        Khác biệt ở chỗ phần tử đầu tiên trong mảng chuỗi chứa thông tin về độ dài chuỗi
    */
    static WCHAR* WCharToXl12Str(const WCHAR* wstrValue) {
        size_t len;
        //Kiểu tra điều kiện tiên quyết của chuỗi đầu vào
        if (!wstrValue || !(len = wcslen(wstrValue))) return NULL;
        //Tiến hành cắt gọt chuỗi theo độ dài tối đa mà Excel quy định
        if (len > 32767u) len = 32767u;
        //Cấp phát bộ nhớ động để lưu trữ kết quả
        WCHAR* wstrRetVal = new WCHAR[len + 2];
        //Nếu hết bộ nhớ thì thoát khỏi hàm
        if (!wstrRetVal) return NULL;
        /*
            Sao chép nội dung của chuỗi đầu vào vào chuỗi kết quả
            Vị trí là phần tử thứ hai của mảng chuỗi cho đến hết
            Lý do là vì Excel yêu cầu phần tử đầu tiên trong mảng chuỗi phải chứa thông tin về độ dài chuỗi
        */
        memcpy(wstrRetVal + 1, wstrValue, (len + 1) * sizeof(WCHAR));
        //Gán thông tin độ dài chuỗi vào phần tử đầu tiên của mảng chuỗi
        wstrRetVal[0] = (WCHAR)len;
        //Trả về kết quả
        return wstrRetVal;
    }
    
    //Đây là hàm sẽ xuất hiện trong bảng tính
    extern "C" __declspec(dllexport) LPXLOPER12 WINAPI AskGemini(WCHAR* lpszPrompt) {
        //Xác định vùng nhớ được cấp phát riêng cho thread
        TlsData* tlsData = GetTlsData();
        if (!tlsData) return NULL;
        //Khai báo biến chứa kết quả, lấy từ vùng nhớ đã cấp phát riêng cho thread
        LPXLOPER12 result = &(tlsData->data);
        size_t len;
        //Kiểm tra các điều kiện tiên quyết của chuỗi đầu vào, nếu không đạt yêu cầu thì báo lỗi
        if (!lpszPrompt || !(len = wcslen(lpszPrompt))) {
            result->val.err = xlerrNA;
            result->xltype = xltypeErr;
            return result;
        }
        //Khai báo class để làm việc với Google Gemini
        Response response{};
        //Khóa API
        Gemini gemini{L"Điền khóa API vào đây"};
        //Khai báo con trỏ trỏ đến vùng nhớ chứa chuỗi thông báo lỗi
        WCHAR* wstrError = NULL;
        //Gửi yêu cầu đi
        HRESULT hr = gemini.AskGemini(lpszPrompt, response, &wstrError);
        //Nếu thất bại
        if (FAILED(hr)) {
            //Xác định thông báo lỗi
            WCHAR* wstrXl12Err = WCharToXl12Str(wstrError);
            //Nếu hết bộ nhớ thì tiến hành dọn dẹp và trả về lỗi
            if (!wstrXl12Err) {
                delete[] wstrError;
                result->val.err = xlerrNA;
                result->xltype = xltypeErr;
                return result;
            }
            //Ngược lại, tiến hành dọn dẹp bộ nhớ và trả về lỗi
            delete[] wstrError;
            result->val.str = (wchar_t*)wstrXl12Err;
            result->xltype = xltypeStr | xlbitDLLFree;
            return result;
        }
        //Kiểm tra xem liệu kết quả trả về có chứa thông tin không
        if (!response.Candidates.size() || !response.Candidates[0].Content.Parts.size()) {
            result->val.err = xlerrNA;
            result->xltype = xltypeErr;
            return result;
        }
        //Xác định thông tin cần lấy
        std::string strAnswer(response.Candidates[0].Content.Parts[0].Text);
        /*
            Do thư viện nlohmann chỉ chấp nhận chuỗi mã hóa UTF-8, cho nên
            Những khối lệnh sử dụng hàm MultiByteToWideChar dưới đây
            Có nhiệm vụ chuyển đổi chuỗi mã hóa UTF-8 thành chuỗi wide string
        */
        //Xác định độ dài chuỗi để chuẩn bị cho việc cấp phát bộ nhớ động
        int nAnswerlen = MultiByteToWideChar(CP_UTF8, 0, strAnswer.c_str(), -1, NULL, 0);
        //Cấp phát bộ nhớ động
        WCHAR* wstrRetVal = new WCHAR[nAnswerlen];
        //Nếu hết bộ nhớ thì thông báo lỗi
        if (!wstrRetVal) {
            result->val.err = xlerrNA;
            result->xltype = xltypeErr;
            return result;
        }
        //Nếu không thể thực hiện việc chuyển đổi thì trả về lỗi
        if (!MultiByteToWideChar(CP_UTF8, 0, strAnswer.c_str(), -1, wstrRetVal, nAnswerlen)) {
            delete[] wstrRetVal;
            result->val.err = xlerrNA;
            result->xltype = xltypeErr;
            return result;
        }
        //Chuyển đổi chuỗi thành dạng mà Excel yêu cầu
        WCHAR* wstrResult = WCharToXl12Str(wstrRetVal);
        //Dọn dẹp bộ nhớ sau khi dùng xong
        delete[] wstrRetVal;
        //Nếu hàm WCharToXl12Str không thể xử lý thì trả về lỗi
        if (!wstrResult) {
            result->val.err = xlerrNA;
            result->xltype = xltypeErr;
            return result;
        }
        /*
            Tổ chức kết quả theo cấu trúc để Excel có thể xử lý được
            xltypeStr tức là mang kiểu chuỗi
            Do kết quả trả về là kiểu chuỗi được cấp phát động
            Cho nên cần phải dùng bit cờ xlbitDLLFree
            Để thông báo cho Excel biết rằng nó cần phải dọn dẹp bộ nhớ sau khi sử dụng xong
            Thông qua hàm gọi lại (callback) xlAutoFree12
        */
        result->xltype = xltypeStr | xlbitDLLFree;
        result->val.str = (WCHAR*)wstrResult;
        return result;
    }
    
    extern "C" __declspec(dllexport) int WINAPI xlAutoOpen(void)
    {
        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, 12, (LPXLOPER12)&xlDllName, //Đăng ký tên của DLL
                (LPXLOPER12)TempStr12(L"AskGemini"), //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"AskGemini"), //Tên hàm trong trình Function Wizard
                (LPXLOPER12)TempStr12(L"Prompt"), //Mô tả tham số trong trình Function Wizard
                (LPXLOPER12)TempStr12(L"1"), //Kiểu macro
                (LPXLOPER12)TempStr12(L"Google AI Assistant"), //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"Ask Gemini for assistance"), //Mô tả hàm khi được chọn trong trình Function Wizard
                (LPXLOPER12)TempStr12(L"Prompt to send to Gemini"), //Mô tả tham số khi được chọn trong trình Function Wizard (tối đa lên đến 255 tham số)
                (LPXLOPER12)TempStr12(L""));
            //Giải phóng biến xDLL
            Excel12f(xlFree, NULL, 1, (LPXLOPER12)&xlDllName);
        }
        return 1;
    }
    
    extern "C" __declspec(dllexport) int WINAPI xlAutoClose(void) {
        //Hủy đăng ký hàm trước khi ngừng sử dụng add-in
        XLOPER12 xlFuncId;
        if (Excel12f(xlfEvaluate, &xlFuncId, 1, (LPXLOPER12)TempStr12(L"AskGemini")) == xlretSuccess) {
            Excel12f(xlfUnregister, NULL, 1, (LPXLOPER12) & xlFuncId);
            Excel12f(xlFree, NULL, 1, (LPXLOPER12)&xlFuncId);
            Excel12f(xlfSetName, NULL, 1, (LPXLOPER12)TempStr12(L"AskGemini"));
        }
        return 1;
    }
    
    /*
        Hàm này Excel sẽ gọi
        Khi kết quả trả về cho Excel chứa một các bit cờ xlbitDLLFree và xlbitXLFree
        Những kiểu như xltypeStr, xltypeRef và xltypeMulti sẽ được gắn cờ xlbitDLLFree
        Còn kiểu XLOPER12 được cấp phát bộ nhớ động thì sẽ được gắn cờ xlbitXLFree
    */
    extern "C" __declspec(dllexport) void WINAPI xlAutoFree12(LPXLOPER12 p_oper) {
        //Nếu kết quả chứa kiểu chuỗi thì dọn dẹp vùng nhớ
        if (p_oper->xltype & xltypeStr)
            delete[] p_oper->val.str;
        //Nếu là mảng
        else if (p_oper->xltype & xltypeMulti) {
            //Xác định kích thước của mảng, hay số lượng phần tử có trong mảng
            int size = p_oper->val.array.columns * p_oper->val.array.rows;
            LPXLOPER12 p = p_oper->val.array.lparray;
            //Tiến hành duyệt qua mảng
            for (; size-- > 0; p++) {
                /*
                    Khi loại trừ các bit cờ xlbitDLLFree và xlbitXLFree
                    Nếu là kiểu chuỗi thì dọn dẹp bộ nhớ
                */
                if ((p->xltype & ~(xlbitDLLFree | xlbitXLFree)) == xltypeStr) {
                    if (p->xltype & xlbitDLLFree)
                        delete[] p->val.str;
                    //Nếu là kiểu XLOPER12 được cấp phát động thì dọn dẹp bộ nhớ
                    else if (p->xltype & xlbitXLFree)
                        Excel12f(xlFree, 0, 1, p);
                }
            }
            //Dọn dẹp bộ nhớ đã cấp phát cho mảng
            delete p_oper->val.array.lparray;
        }
        //Những trường hợp cần dọn dẹp bộ nhớ còn lại
        else if (p_oper->xltype & xltypeRef)
            delete p_oper->val.mref.lpmref;
        else if (p_oper->xltype & xlbitDLLFree) delete p_oper;
    }
    
    //Hàm này đăng ký thông tin add-in với Excel
    extern "C" __declspec(dllexport) LPXLOPER12 WINAPI xlAddInManagerInfo12(LPXLOPER12 pxAction) {
        TlsData* pTlsData = GetTlsData();
        if (!pTlsData) return NULL;
        LPXLOPER12 lpResult = &(pTlsData->data);
        LPXLOPER12 lpIntAction = &(pTlsData->data2);
        if (Excel12f(xlCoerce, lpIntAction, 1, pxAction) == xlretSuccess) {
            if (lpIntAction->val.w == 1) {
                lpResult->xltype = xltypeStr;
                lpResult->val.str = (WCHAR*) L"Google Gemini Assistant";
            }
            else {
                lpResult->xltype = xltypeErr;
                lpResult->val.err = xlerrValue;
            }
        }
        return lpResult;
    }

    Response.h:

    C++:
    #pragma once
    #include <string>
    #include <vector>
    
    struct Part {
        std::string Text;
    };
    
    struct Content {
        std::vector<Part>Parts;
        std::string Role;
    };
    
    struct Candidate {
        Content Content;
        std::string FinishReason;
        float AvgLogprobs;
    };
    
    struct CandidatesTokensDetail {
        std::string Modality;
        unsigned long TokenCount;
    };
    
    struct PromptTokensDetail {
        std::string Modality;
        unsigned long TokenCount;
    };
    
    struct UsageMetadata {
        unsigned long PromptTokenCount;
        unsigned long CandidatesTokenCount;
        unsigned long TotalTokenCount;
        std::vector<PromptTokensDetail>PromptTokensDetails;
        std::vector< CandidatesTokensDetail> CandidatesTokensDetails;
    };
    
    struct Response {
        std::vector<Candidate>Candidates;
        UsageMetadata UsageMetadata;
        std::string ModelVersion;
    };

    Sử dụng công thức trong Excel:
    1746117683856.png

    Người dùng có thể tìm thấy code đầy đủ đính kèm theo bài viết này.
     

    File đính kèm

    Lần chỉnh sửa cuối:
    Cái này mình thử dùng VBA (không dài lắm) kèm theo file JsonConverter.bas từ Github cũng ra

    1.png
     
    Cái này mình thử dùng VBA (không dài lắm) kèm theo file JsonConverter.bas từ Github cũng ra

    View attachment 308067
    VBA có nhược điểm là không được Excel xem là an toàn thread (thread-safe) do bản chất VBA không hỗ trợ đa luồng, cho nên khi bạn sử dụng nhiều hàm viết bằng VBA trên bảng tính, Excel chỉ có thể tính toán tuần tự từng hàm, như trong hình các công thức từ F2 đến F4, công thức nào tính toán xong thì mới đến lượt công thức khác.
    Trong bài viết Multithreaded recalculation in Excel, Excel liệt kê những hành vi nào được xem là an toàn thread và không an toàn thread.

    1746488704599.png

    Hàm AskGemini trong bài viết của mình chưa đăng ký an toàn thread, mặc dù nó đã đáp ứng điều kiện để trở thành hàm an toàn thread.
     
    Tiếp nối bài viết Thử lập trình XLL add-in bằng C/C++, nay tôi xin giới thiệu một ví dụ về việc sử dụng XLL add-in để tương tác với Google Gemini dưới dạng công thức bảng tính.
    Google Gemini là một chat bot do Google phát triển nhằm chạy đua với ChatGPT của OpenAI, Microsoft Copilot của Microsoft và nhiều hãng công nghệ khác. Khởi đầu với cái tên Google Bard, nay nó đã phát triển vượt bậc với cái tên Gemini và tỏ ra không hề kém cạnh so với các sản phẩm chat bot đình đám hiện nay.
    Tương tự như nhiều sản phẩm khác, Google cho phép tích hợp chat bot này vào các ứng dụng khác nhau dưới dạng API, dưới dạng gói dịch vụ miễn phí và trả phí. Để biết thêm thông tin chi tiết, người dùng có thể tham khảo bài viết này: Google AI Studio quickstart.
    Sau khi đã tạo tài khoản và/hoặc đăng nhập, người dùng có thể lấy khóa API tại đây: Quickly test the Gemini API
    Dưới đây là toàn bộ code về XLL add-in trong bài viết này, mấu chốt nằm ở class Gemini khi nó đảm nhận các công việc then chốt như tạo, gửi yêu cầu, xử lý kết quả trả về cho Excel hiển thị. Code đều được chú thích cụ thể, rõ ràng để mọi người có thể dễ theo dõi và tránh cho bài viết này dài dòng và lan man.
    Lưu ý: Khóa API được gán trực tiếp vào code, điều này có thể không an toàn do người có chuyên môn có thể soi thấy khóa API, cho nên cách tốt nhất là người dùng nên sửa lại code cho phép lấy thông tin khóa API từ tập tin bên ngoài.

    Gemini.h:

    C++:
    #pragma once
    #import <winhttpcom.dll>
    #include <comdef.h>
    #include <comutil.h>
    #include "nlohmann/json.hpp"
    #include <vector>
    #include "Response.h"
    
    class Gemini
    {
    public:
        Gemini(PCWSTR lpszKey);
        virtual HRESULT AskGemini(PCWSTR szPrompt, Response& response, WCHAR** lppszError);
        BOOL ApiKey(PCWSTR lpszValue);
        PCWSTR GetApiKey() const;
    private:
        PCWSTR lpszApiKey = L"";
    };

    Gemini.cpp:
    C++:
    #include "Gemini.h"
    
    Gemini::Gemini(PCWSTR lpszKey) {
        size_t len;
        if (!lpszKey || !(len = wcslen(lpszKey))) lpszApiKey = L"";
        else lpszApiKey = lpszKey;
    }
    
    BOOL Gemini::ApiKey(PCWSTR lpszValue) {
        if (!lpszValue) return FALSE;
        lpszApiKey = lpszValue;
        return lpszApiKey ? TRUE : FALSE;
    }
    
    PCWSTR Gemini::GetApiKey() const {
        return lpszApiKey;
    }
    
    /*
        Hàm này chuyển đổi kiểu chuỗi BSTR thành kiểu chuỗi WCHAR (hay wchar_t)
        BSTR hay WCHAR thực chất đều giống nhau,
        chỉ khác ở chỗ BSTR quy định 4 byte đầu tiên chứa thông tin về độ dài chuỗi
        Tham khảo thêm: https://learn.microsoft.com/en-us/previous-versions/windows/desktop/automat/bstr
    */
    static WCHAR* BstrToWchar(BSTR bstrVal) {
        size_t len;
        //Nếu biến bstrVal là con trỏ không hợp lệ (0) hoặc độ dài chuỗi là 0 thì trả về NULL (0)
        if (!bstrVal || !(len = SysStringLen(bstrVal))) return NULL;
        //Cấp phát bộ nhớ động cho chuỗi kết quả, chừa ra 2 byte để chứa ký tự rỗng kết thúc
        wchar_t* wstrRetVal = new wchar_t[len + 1];
        //Nếu không thể cấp phát bộ nhớ tức là hết bộ nhớ thì trả về NULL
        if (!wstrRetVal) return NULL;
        //Sao chép nội dung mà bstrVal trỏ đến vào biến wstrRetVal
        memcpy(wstrRetVal, bstrVal, len * sizeof(WCHAR));
        //Gán ký tự tự rỗng kết thúc vào vị trí cuối cùng của mảng chuỗi, sau đó trả về kết qủa
        wstrRetVal[len] = '\0';
        return wstrRetVal;
    }
    
    /*
        Hàm này chuyển đổi kiểu chuỗi BSTR thành chuỗi mã hóa (encode) UTF-8
        Để các hàm xử lý chuỗi JSON có thể làm việc
    */
    static char* BstrToChar(BSTR bstrVal) {
        size_t len;
        //Nếu biến bstrVal là con trỏ không hợp lệ (0) hoặc độ dài chuỗi là 0 thì trả về NULL (0)
        if (!bstrVal || !(len = SysStringLen(bstrVal))) return NULL;
        //Xác định độ dài chuỗi cần thiết để chuẩn bị cho việc cấp phát bộ nhớ động
        int strLen = WideCharToMultiByte(CP_UTF8, 0, bstrVal, (int)len, NULL, 0, NULL, NULL);
        //Tiến hành cấp phát bộ nhớ động, chừa ra một byte để đánh dấu ký tự rỗng kết thúc
        char* strRetVal = new char[strLen + 1];
        //Nếu không thể cấp phát bộ nhớ tức là hết bộ nhớ thì trả về NULL
        if (!strRetVal) return NULL;
        //Trường hợp hàm WideCharToMultiByte trả về 0, nghĩa là quá trình chuyển đổi không thành công
        if (!WideCharToMultiByte(CP_UTF8, 0, bstrVal, (int)len, strRetVal, strLen, NULL, NULL)) {
            //Giải phóng bộ nhớ động đã cấp phát, sau đó thoát khỏi hàm
            delete[] strRetVal;
            return NULL;
        }
        //Gán ký tự tự rỗng kết thúc vào vị trí cuối cùng của mảng chuỗi, sau đó trả về kết quả
        strRetVal[strLen] = '\0';
        return strRetVal;
    }
    
    /*
        Hàm này chuyển đổi ký tự mã hóa UTF-8 thành mảng byte (byte array)
    */
    static HRESULT CharToSafeArray(LPCSTR strVal, SAFEARRAY** ppsa) {
        //Nếu strVal là con trỏ không hợp lệ thì thoát khỏi hàm
        if (!strVal) return E_POINTER;
        //Xác định độ dài chuỗi
        size_t len = strlen(strVal);
        //Nếu là chuỗi rỗng (hay "") thì thoát khỏi hàm
        if (!len) return E_INVALIDARG;
        HRESULT hr;
        //Tiến hành khai báo kích thước mảng, cận dưới là 0 và số lượng phần tử tương ứng với độ dài chuỗi
        SAFEARRAYBOUND sab[1]{};
        sab[0].cElements = (ULONG)len;
        sab[0].lLbound = 0;
        /*
            Khai báo mảng SAFEARRAY
            Để biểu diễn một dãy byte thì chỉ cần kiểu số không âm VT_UI1 (hay ushort) là đủ
        */
        SAFEARRAY* psa = SafeArrayCreate(VT_UI1, 1, sab);
        //Nếu không thể khởi tạo mảng thì nghĩa là đã hết bộ nhớ
        if (!psa) return E_OUTOFMEMORY;
        //Khai báo biến lpData để truy cập mảng
        LPVOID lpData = NULL;
        //Tiến hành khóa và truy cập mảng
        hr = SafeArrayAccessData(psa, &lpData);
        //Nếu không thể khóa truy cập mảng thì tiến hành dọn dẹp bộ nhớ và thoát khỏi hàm
        if (FAILED(hr)) {
            SafeArrayDestroy(psa);
            return hr;
        }
        //Sao chép nội dung của biến strVal vào mảng
        memcpy(lpData, strVal, len);
        //Mở khóa truy cập mảng
        hr = SafeArrayUnaccessData(psa);
        //Nếu không thể mở khóa truy cập mảng thì tiến hành dọn dẹp bộ nhớ và thoát khỏi hàm
        if (FAILED(hr)) {
            SafeArrayDestroy(psa);
            return hr;
        }
        //Gán con trỏ kết quả
        *ppsa = psa;
        //Thông báo rằng hoạt động đã diễn ra thành công
        return S_OK;
    }
    //Các hàm ParsePart, ParseContent, ParseCandidate và ParseResponse chịu trách nhiệm xử lý chuỗi JSON
    static HRESULT ParsePart(const nlohmann::json& json, Part& part) {
        try {
            if (json.empty()) return E_INVALIDARG;
            if (json.contains("text") && !json.at("text").empty() && !json.at("text").is_null() && json.at("text").is_string())
                json.at("text").get_to<std::string>(part.Text);
        }
        catch (const nlohmann::json::type_error&) {
            return E_FAIL;
        }
        return S_OK;
    }
    
    static HRESULT ParseContent(const nlohmann::json& json, Content& result) {
        try {
            if (json.empty()) return E_INVALIDARG;
            if (json.contains("role") && !json.at("role").empty() && !json.at("role").is_null() && json.at("role").is_string())
                json.at("role").get_to<std::string>(result.Role);
            if (json.contains("parts") && !json.at("parts").empty() && !json.at("parts").is_null() && json.at("parts").is_array()) {
                std::vector<Part>parts;
                for (auto i = json["parts"].rbegin(); i < json["parts"].rend(); i++) {
                    nlohmann::json item = *i;
                    Part part{};
                    HRESULT hr = ParsePart(item, part);
                    if (FAILED(hr)) return hr;
                    parts.push_back(part);
                }
                result.Parts = parts;
            }
        }
        catch (const nlohmann::json::type_error&) {
            return E_FAIL;
        }
        return S_OK;
    }
    
    static HRESULT ParseCandidate(const nlohmann::json& json, Candidate& result) {
        try {
            if (json.empty()) return E_INVALIDARG;
            if (json.contains("finishReason") && !json.at("finishReason").empty() && !json.at("finishReason").is_null() && json.at("finishReason").is_string())
                json.at("finishReason").get_to<std::string>(result.FinishReason);
            if (json.contains("avgLogprobs") && !json.at("avgLogprobs").empty() && !json.at("avgLogprobs").is_null() && json.at("avgLogprobs").is_number_float())
                json.at("avgLogprobs").get_to<float>(result.AvgLogprobs);
            if (json.contains("content") && !json.at("content").empty() && !json.at("content").is_null() && !json.at("content").is_array()) {
                Content content{};
                nlohmann::json e = json["content"];
                HRESULT hr = ParseContent(e, content);
                if (FAILED(hr)) return hr;
                result.Content = content;
            }
        }
        catch (const nlohmann::json::type_error&) {
            return E_FAIL;
        }
        return S_OK;
    }
    
    static HRESULT ParseResponse(BSTR bstrResponse, Response& result) {
        if (!bstrResponse) return E_POINTER;
        size_t len = SysStringLen(bstrResponse);
        if (!len) return E_INVALIDARG;
        char* strResponse = BstrToChar(bstrResponse);
        if (!strResponse) return E_OUTOFMEMORY;
        Response response{};
        try {
            nlohmann::json json = nlohmann::json::parse(strResponse);
            delete[] strResponse;
            if (json.contains("candidates") && !json.at("candidates").is_null() && json.at("candidates").is_array()) {
                std::vector<Candidate>candidates;
                for (auto i = json["candidates"].rbegin(); i < json["candidates"].rend(); i++) {
                    nlohmann::json item = *i;
                    Candidate candidate{};
                    HRESULT hr = ParseCandidate(item, candidate);
                    if (FAILED(hr)) return hr;
                    candidates.push_back(candidate);
                }
                result.Candidates = candidates;
            }
            if (json.contains("modelVersion") && !json.at("modelVersion").is_null() && json.at("modelVersion").is_string())
                json.at("modelVersion").get_to<std::string>(result.ModelVersion);
        }
        catch (const nlohmann::json::type_error&) {
            delete[] strResponse;
            return E_FAIL;
        }
        return S_OK;
    }
    
    /*
        Hàm này có nhiệm vụ chính sau:
        1. Chuẩn bị chuỗi JSON để gửi đi thông qua thư viện WinHTTP COM
        2. Xử lý chuỗi JSON nhận được và trả kết quả
    */
    HRESULT Gemini::AskGemini(PCWSTR lpszPrompt, Response& response, WCHAR** lppszError) {
        using namespace WinHttp;
        size_t nPromptLen, nApiKeyLen;
        //Kiểm tra các con trỏ đầu vào phải đạt yêu cầu, nếu không phải thoát khỏi hàm
        if (!lpszPrompt || (nPromptLen = lstrlen(lpszPrompt)) == 0 || !lpszApiKey || (nApiKeyLen = lstrlen(lpszApiKey)) == 0) return E_POINTER;
        /*
            Xác định độ dài chuỗi cần thiết để chuẩn bị cấp phát bộ nhớ động
            Lý do sử dụng hàm WideCharToMultiByte là vì
            Thư viện nlohmann chỉ hỗ trợ xử lý chuỗi JSON dưới dạng mã hóa UTF-8
        */
        int nPromptNumChars = WideCharToMultiByte(CP_UTF8, 0, lpszPrompt, (int)nPromptLen, NULL, 0, NULL, NULL);
        //Cấp phát bộ nhớ động
        char* szUtf8Prompt = new char[nPromptNumChars + 1];
        //Nếu không thể cấp phát bộ nhớ tức là đã hết bộ nhớ, cần thoát khỏi hàm
        if (!szUtf8Prompt) return E_OUTOFMEMORY;
        //Tiến hành chuyển đổi chuỗi sang dạng mã hóa UTF-8, nếu không thành công thì dọn dẹp bộ nhớ và thoát khỏi hàm
        if (WideCharToMultiByte(CP_UTF8, 0, lpszPrompt, (int)nPromptLen, szUtf8Prompt, nPromptNumChars, NULL, NULL) == 0) {
            delete[] szUtf8Prompt;
            return E_FAIL;
        }
        //Gán ký tự rỗng kết thúc vào phần tử cuối cùng của mảng chuỗi
        szUtf8Prompt[nPromptNumChars] = '\0';
        try {
            //Khối lệnh này chịu trách nhiệm khởi tạo đối tượng JSON để gửi yêu cầu đến Google Gemini
            nlohmann::json jsonPart;
            jsonPart["text"] = szUtf8Prompt;
            delete[] szUtf8Prompt;
            nlohmann::json jsonParts = nlohmann::json::array({ jsonPart });
            nlohmann::json jsonContent;
            jsonContent["parts"] = jsonParts;
            nlohmann::json jsonContents = nlohmann::json::array({ jsonContent });
            nlohmann::json json;
            json["contents"] = jsonContents;
            //Khởi tạo thư viện COM
            HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE);
            if (FAILED(hr)) return hr;
            //Khởi tạo đối tượng dùng để gửi yêu cầu đến Google Gemini
            IWinHttpRequestPtr pWinHttp;
            hr = pWinHttp.CreateInstance(__uuidof(WinHttpRequest));
            //Nếu không thể khởi tạo đối tượng thì thoát khỏi hàm
            if (FAILED(hr)) return hr;
            /*
                Chuẩn bị URL để kết nối với Google Gemini
                Chuẩn bị các tham số cần thiết
            */
            _bstr_t bstrUrl("https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=" + _bstr_t(lpszApiKey));
            hr = pWinHttp->Open(_bstr_t("POST"), bstrUrl, VARIANT_TRUE);
            if (FAILED(hr)) {
                CoUninitialize();
                return hr;
            }
            hr = pWinHttp->SetRequestHeader(_bstr_t("Content-Type"), _bstr_t("application/json"));
            if (FAILED(hr)) {
                CoUninitialize();
                return hr;
            }
            //Chuyển đối tượng JSON thành dạng chuỗi
            std::string strJson(json.dump());
            //Khởi tạo mảng SAFEARRAY chứa chuỗi JSON ở dạng mảng byte
            SAFEARRAY* psa = NULL;
            hr = CharToSafeArray(strJson.c_str(), &psa);
            if (FAILED(hr)) {
                CoUninitialize();
                return hr;
            }
            VARIANT vtRequestBody{};
            VariantInit(&vtRequestBody);
            vtRequestBody.vt = VT_ARRAY | VT_UI1;
            vtRequestBody.parray = psa;
            //Gửi yêu cầu đi, nếu hoạt động không thành công thì dọn dẹp bộ nhớ và thoát khỏi hàm
            hr = pWinHttp->Send(vtRequestBody);
            if (FAILED(hr)) {
                CoUninitialize();
                VariantClear(&vtRequestBody);
                return hr;
            }
            //Dọn dẹp bộ nhớ sau khi dùng xong
            VariantClear(&vtRequestBody);
            /*
                Nếu quá thời gian 30 giây mà vẫn chưa nhận được phản hồi từ Google Gemini
                Xuất ra chuỗi thông báo, sau đó dọn dẹp bộ nhớ và thoát khỏi hàm
            */
            if (!pWinHttp->WaitForResponse(30)) {
                WCHAR lpszRequestTimedOut[] = L"Error: Request timed out";
                size_t len = wcslen(lpszRequestTimedOut);
                WCHAR* lpszText = new WCHAR[len + 1];
                if (!lpszText) return NULL;
                memcpy(lpszText, lpszRequestTimedOut, len * sizeof(WCHAR));
                lpszText[len] = '\0';
                *lppszError = lpszText;
                CoUninitialize();
                return E_FAIL;
            }
            //Kiểm tra status code trả về
            long nStatus = pWinHttp->GetStatus();
            //Nếu trả về mã 200 tức là yêu cầu đã thành công
            if (nStatus == 200) {
                //Lưu trữ kết quả dạng chuỗi
                _bstr_t bstrResponse(pWinHttp->GetResponseText());
                //Gọi hàm chuyển đổi (deserialize), hay giải tuần tự hóa, đối tượng JSON thành dạng struct
                hr = ParseResponse(bstrResponse, response);
                if (FAILED(hr)) {
                    CoUninitialize();
                    return E_FAIL;
                }
            }
            //Trường hợp trả về mã khác
            else {
                //Xuất ra chuỗi thông báo lỗi, dọn dẹp bộ nhớ và thoát khỏi hàm
                _bstr_t bstrError(pWinHttp->GetResponseText());
                WCHAR* wstrError = BstrToWchar(bstrError);
                *lppszError = wstrError;
                CoUninitialize();
                return E_FAIL;
            }
            CoUninitialize();
        }
        //Xử lý ngoại lệ không thể xử lý chuỗi thành đối tượng JSON
        catch (const std::exception&) {
            return E_FAIL;
        }
        //Thông báo rằng hoạt động đã diễn ra thành công
        return S_OK;
    }

    dllmain.cpp:

    C++:
    #include "XLCALL.H"
    #include "FRAMEWRK.H"
    #include "string"
    #include "Gemini.h"
    #include "framework.h"
    
    #pragma comment(lib, "XLCALL32.LIB")
    
    /*
        Khai báo biến lưu trữ chỉ mục giúp phân biệt vùng nhớ cục bộ cho mỗi thread
        Điều này cần thiết khi Excel áp dụng tính năng multithreaded recalculation, multithread calculation
        Tức là sử dụng đa luồng để tính toán các công thức trong bảng tính
        Lúc này mỗi thread sẽ được cấp phát động một vùng nhớ riêng biệt
        Giúp đảm bảo tính an toàn thread (thread safety)
    */
    DWORD dwTlsIndex;
    
    //Khai báo cấu trúc dữ liệu cần dùng
    struct TlsData {
        XLOPER12 data;
        XLOPER12 data2;
    };
    
    //Hàm này thực hiện việc cấp phát, trả về con trỏ trỏ đến vùng nhớ theo chỉ mục được gán cho mỗi thread
    TlsData* GetTlsData(void) {
        //Khởi tạo và xác định vùng nhớ đã được cấp phát
        TlsData* pTlsData = reinterpret_cast<TlsData*>(TlsGetValue(dwTlsIndex));
        //Nếu chưa được cấp phát bộ nhớ thì tiến hành cấp phát động bộ nhớ
        if (!pTlsData) {
            //Tiến hành cấp phát động bộ nhớ
            pTlsData = new TlsData[1];
            //Trường hợp hết bộ nhớ thì thoát khỏi hàm
            if (!pTlsData) return NULL;
            //Ngược lại, gán vùng nhớ được cấp phát động theo chỉ mục
            TlsSetValue(dwTlsIndex, pTlsData);
        }
        //Trả về con trỏ trỏ đến vùng nhớ
        return pTlsData;
    }
    
    //Hàm này khởi tạo thread-local storage, hay vùng nhớ cục bộ của mỗi thread
    BOOL InitializeTls(DWORD DllMainCallReason) {
        //Tùy thuộc vào trạng thái khi Excel sử dụng/ngừng sử dụng DLL
        switch (DllMainCallReason) {
        case DLL_PROCESS_ATTACH:
            //Khi Excel sử dụng DLL thì tiến hành cấp chỉ số cho thread
            if ((dwTlsIndex = TlsAlloc()) == TLS_OUT_OF_INDEXES)
                return FALSE;
            break;
            //Khi Excel ngừng sử dụng DLL thì tiến hành dọn dẹp vùng nhớ đã cấp cho thread theo chỉ mục
        case DLL_PROCESS_DETACH:
            TlsFree(dwTlsIndex);
            break;
        }
        return TRUE;
    }
    
    BOOL APIENTRY DllMain( HMODULE hModule,
                           DWORD  ul_reason_for_call,
                           LPVOID lpReserved
                         )
    {
        return InitializeTls(ul_reason_for_call);
    }
    
    /*
        Hàm này chuyển đổi chuỗi wide string thành dạng wide string mà Excel chấp nhận
        Dạng chuỗi wide string mà Excel chấp nhận cơ bản không khác gì mấy so với dạng wide string thông thường
        Khác biệt ở chỗ phần tử đầu tiên trong mảng chuỗi chứa thông tin về độ dài chuỗi
    */
    static WCHAR* WCharToXl12Str(const WCHAR* wstrValue) {
        size_t len;
        //Kiểu tra điều kiện tiên quyết của chuỗi đầu vào
        if (!wstrValue || !(len = wcslen(wstrValue))) return NULL;
        //Tiến hành cắt gọt chuỗi theo độ dài tối đa mà Excel quy định
        if (len > 32767u) len = 32767u;
        //Cấp phát bộ nhớ động để lưu trữ kết quả
        WCHAR* wstrRetVal = new WCHAR[len + 2];
        //Nếu hết bộ nhớ thì thoát khỏi hàm
        if (!wstrRetVal) return NULL;
        /*
            Sao chép nội dung của chuỗi đầu vào vào chuỗi kết quả
            Vị trí là phần tử thứ hai của mảng chuỗi cho đến hết
            Lý do là vì Excel yêu cầu phần tử đầu tiên trong mảng chuỗi phải chứa thông tin về độ dài chuỗi
        */
        memcpy(wstrRetVal + 1, wstrValue, (len + 1) * sizeof(WCHAR));
        //Gán thông tin độ dài chuỗi vào phần tử đầu tiên của mảng chuỗi
        wstrRetVal[0] = (WCHAR)len;
        //Trả về kết quả
        return wstrRetVal;
    }
    
    //Đây là hàm sẽ xuất hiện trong bảng tính
    extern "C" __declspec(dllexport) LPXLOPER12 WINAPI AskGemini(WCHAR* lpszPrompt) {
        //Xác định vùng nhớ được cấp phát riêng cho thread
        TlsData* tlsData = GetTlsData();
        if (!tlsData) return NULL;
        //Khai báo biến chứa kết quả, lấy từ vùng nhớ đã cấp phát riêng cho thread
        LPXLOPER12 result = &(tlsData->data);
        size_t len;
        //Kiểm tra các điều kiện tiên quyết của chuỗi đầu vào, nếu không đạt yêu cầu thì báo lỗi
        if (!lpszPrompt || !(len = wcslen(lpszPrompt))) {
            result->val.err = xlerrNA;
            result->xltype = xltypeErr;
            return result;
        }
        //Khai báo class để làm việc với Google Gemini
        Response response{};
        //Khóa API
        Gemini gemini{L"Điền khóa API vào đây"};
        //Khai báo con trỏ trỏ đến vùng nhớ chứa chuỗi thông báo lỗi
        WCHAR* wstrError = NULL;
        //Gửi yêu cầu đi
        HRESULT hr = gemini.AskGemini(lpszPrompt, response, &wstrError);
        //Nếu thất bại
        if (FAILED(hr)) {
            //Xác định thông báo lỗi
            WCHAR* wstrXl12Err = WCharToXl12Str(wstrError);
            //Nếu hết bộ nhớ thì tiến hành dọn dẹp và trả về lỗi
            if (!wstrXl12Err) {
                delete[] wstrError;
                result->val.err = xlerrNA;
                result->xltype = xltypeErr;
                return result;
            }
            //Ngược lại, tiến hành dọn dẹp bộ nhớ và trả về lỗi
            delete[] wstrError;
            result->val.str = (wchar_t*)wstrXl12Err;
            result->xltype = xltypeStr | xlbitDLLFree;
            return result;
        }
        //Kiểm tra xem liệu kết quả trả về có chứa thông tin không
        if (!response.Candidates.size() || !response.Candidates[0].Content.Parts.size()) {
            result->val.err = xlerrNA;
            result->xltype = xltypeErr;
            return result;
        }
        //Xác định thông tin cần lấy
        std::string strAnswer(response.Candidates[0].Content.Parts[0].Text);
        /*
            Do thư viện nlohmann chỉ chấp nhận chuỗi mã hóa UTF-8, cho nên
            Những khối lệnh sử dụng hàm MultiByteToWideChar dưới đây
            Có nhiệm vụ chuyển đổi chuỗi mã hóa UTF-8 thành chuỗi wide string
        */
        //Xác định độ dài chuỗi để chuẩn bị cho việc cấp phát bộ nhớ động
        int nAnswerlen = MultiByteToWideChar(CP_UTF8, 0, strAnswer.c_str(), -1, NULL, 0);
        //Cấp phát bộ nhớ động
        WCHAR* wstrRetVal = new WCHAR[nAnswerlen];
        //Nếu hết bộ nhớ thì thông báo lỗi
        if (!wstrRetVal) {
            result->val.err = xlerrNA;
            result->xltype = xltypeErr;
            return result;
        }
        //Nếu không thể thực hiện việc chuyển đổi thì trả về lỗi
        if (!MultiByteToWideChar(CP_UTF8, 0, strAnswer.c_str(), -1, wstrRetVal, nAnswerlen)) {
            delete[] wstrRetVal;
            result->val.err = xlerrNA;
            result->xltype = xltypeErr;
            return result;
        }
        //Chuyển đổi chuỗi thành dạng mà Excel yêu cầu
        WCHAR* wstrResult = WCharToXl12Str(wstrRetVal);
        //Dọn dẹp bộ nhớ sau khi dùng xong
        delete[] wstrRetVal;
        //Nếu hàm WCharToXl12Str không thể xử lý thì trả về lỗi
        if (!wstrResult) {
            result->val.err = xlerrNA;
            result->xltype = xltypeErr;
            return result;
        }
        /*
            Tổ chức kết quả theo cấu trúc để Excel có thể xử lý được
            xltypeStr tức là mang kiểu chuỗi
            Do kết quả trả về là kiểu chuỗi được cấp phát động
            Cho nên cần phải dùng bit cờ xlbitDLLFree
            Để thông báo cho Excel biết rằng nó cần phải dọn dẹp bộ nhớ sau khi sử dụng xong
            Thông qua hàm gọi lại (callback) xlAutoFree12
        */
        result->xltype = xltypeStr | xlbitDLLFree;
        result->val.str = (WCHAR*)wstrResult;
        return result;
    }
    
    extern "C" __declspec(dllexport) int WINAPI xlAutoOpen(void)
    {
        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, 12, (LPXLOPER12)&xlDllName, //Đăng ký tên của DLL
                (LPXLOPER12)TempStr12(L"AskGemini"), //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"AskGemini"), //Tên hàm trong trình Function Wizard
                (LPXLOPER12)TempStr12(L"Prompt"), //Mô tả tham số trong trình Function Wizard
                (LPXLOPER12)TempStr12(L"1"), //Kiểu macro
                (LPXLOPER12)TempStr12(L"Google AI Assistant"), //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"Ask Gemini for assistance"), //Mô tả hàm khi được chọn trong trình Function Wizard
                (LPXLOPER12)TempStr12(L"Prompt to send to Gemini"), //Mô tả tham số khi được chọn trong trình Function Wizard (tối đa lên đến 255 tham số)
                (LPXLOPER12)TempStr12(L""));
            //Giải phóng biến xDLL
            Excel12f(xlFree, NULL, 1, (LPXLOPER12)&xlDllName);
        }
        return 1;
    }
    
    extern "C" __declspec(dllexport) int WINAPI xlAutoClose(void) {
        //Hủy đăng ký hàm trước khi ngừng sử dụng add-in
        XLOPER12 xlFuncId;
        if (Excel12f(xlfEvaluate, &xlFuncId, 1, (LPXLOPER12)TempStr12(L"AskGemini")) == xlretSuccess) {
            Excel12f(xlfUnregister, NULL, 1, (LPXLOPER12) & xlFuncId);
            Excel12f(xlFree, NULL, 1, (LPXLOPER12)&xlFuncId);
            Excel12f(xlfSetName, NULL, 1, (LPXLOPER12)TempStr12(L"AskGemini"));
        }
        return 1;
    }
    
    /*
        Hàm này Excel sẽ gọi
        Khi kết quả trả về cho Excel chứa một các bit cờ xlbitDLLFree và xlbitXLFree
        Những kiểu như xltypeStr, xltypeRef và xltypeMulti sẽ được gắn cờ xlbitDLLFree
        Còn kiểu XLOPER12 được cấp phát bộ nhớ động thì sẽ được gắn cờ xlbitXLFree
    */
    extern "C" __declspec(dllexport) void WINAPI xlAutoFree12(LPXLOPER12 p_oper) {
        //Nếu kết quả chứa kiểu chuỗi thì dọn dẹp vùng nhớ
        if (p_oper->xltype & xltypeStr)
            delete[] p_oper->val.str;
        //Nếu là mảng
        else if (p_oper->xltype & xltypeMulti) {
            //Xác định kích thước của mảng, hay số lượng phần tử có trong mảng
            int size = p_oper->val.array.columns * p_oper->val.array.rows;
            LPXLOPER12 p = p_oper->val.array.lparray;
            //Tiến hành duyệt qua mảng
            for (; size-- > 0; p++) {
                /*
                    Khi loại trừ các bit cờ xlbitDLLFree và xlbitXLFree
                    Nếu là kiểu chuỗi thì dọn dẹp bộ nhớ
                */
                if ((p->xltype & ~(xlbitDLLFree | xlbitXLFree)) == xltypeStr) {
                    if (p->xltype & xlbitDLLFree)
                        delete[] p->val.str;
                    //Nếu là kiểu XLOPER12 được cấp phát động thì dọn dẹp bộ nhớ
                    else if (p->xltype & xlbitXLFree)
                        Excel12f(xlFree, 0, 1, p);
                }
            }
            //Dọn dẹp bộ nhớ đã cấp phát cho mảng
            delete p_oper->val.array.lparray;
        }
        //Những trường hợp cần dọn dẹp bộ nhớ còn lại
        else if (p_oper->xltype & xltypeRef)
            delete p_oper->val.mref.lpmref;
        else if (p_oper->xltype & xlbitDLLFree) delete p_oper;
    }
    
    //Hàm này đăng ký thông tin add-in với Excel
    extern "C" __declspec(dllexport) LPXLOPER12 WINAPI xlAddInManagerInfo12(LPXLOPER12 pxAction) {
        TlsData* pTlsData = GetTlsData();
        if (!pTlsData) return NULL;
        LPXLOPER12 lpResult = &(pTlsData->data);
        LPXLOPER12 lpIntAction = &(pTlsData->data2);
        if (Excel12f(xlCoerce, lpIntAction, 1, pxAction) == xlretSuccess) {
            if (lpIntAction->val.w == 1) {
                lpResult->xltype = xltypeStr;
                lpResult->val.str = (WCHAR*) L"Google Gemini Assistant";
            }
            else {
                lpResult->xltype = xltypeErr;
                lpResult->val.err = xlerrValue;
            }
        }
        return lpResult;
    }

    Response.h:

    C++:
    #pragma once
    #include <string>
    #include <vector>
    
    struct Part {
        std::string Text;
    };
    
    struct Content {
        std::vector<Part>Parts;
        std::string Role;
    };
    
    struct Candidate {
        Content Content;
        std::string FinishReason;
        float AvgLogprobs;
    };
    
    struct CandidatesTokensDetail {
        std::string Modality;
        unsigned long TokenCount;
    };
    
    struct PromptTokensDetail {
        std::string Modality;
        unsigned long TokenCount;
    };
    
    struct UsageMetadata {
        unsigned long PromptTokenCount;
        unsigned long CandidatesTokenCount;
        unsigned long TotalTokenCount;
        std::vector<PromptTokensDetail>PromptTokensDetails;
        std::vector< CandidatesTokensDetail> CandidatesTokensDetails;
    };
    
    struct Response {
        std::vector<Candidate>Candidates;
        UsageMetadata UsageMetadata;
        std::string ModelVersion;
    };

    Sử dụng công thức trong Excel:
    View attachment 308036

    Người dùng có thể tìm thấy code đầy đủ đính kèm theo bài viết này.
    Mình chạy đoạn code bên trên nhưng khi mở test hàm AskGemini thì bị lỗi
    1758012898247.png
     

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

    Back
    Top Bottom