You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

C++/CLI项目调用libcurl下载崩溃及进度更新异常问题

问题环境配置
Config:
Windows 11 x64
Visual Studio Project: CLR Empty Project (.NET Framework)
.NET Framework: 4.6.2
libcurl: curl-7.83.1 windows build (libcurl-vc-x86-release-static-ipv6-sspi-schannel)
Project architecture: Release x86
问题描述

在Windows Forms应用中使用libcurl实现下载进度上报功能时,调用System::Runtime::InteropServices::Marshal::GetFunctionPointerForDelegate完成托管到非托管的指针传递后触发异常,相关代码如下:

Program.cpp 代码

#include "MyForm.h"
[System::STAThread]
int main(array<System::String^>^ args) {
    try {
        System::Windows::Forms::Application::SetUnhandledExceptionMode(System::Windows::Forms::UnhandledExceptionMode::ThrowException);
        System::Windows::Forms::Application::EnableVisualStyles();
        System::Windows::Forms::Application::SetCompatibleTextRenderingDefault(false);
        DownloadProgressTest::MyForm updater;
        System::Windows::Forms::Application::Run(% updater);
        return 0;
    }
    catch (System::Exception^ exception) {
        System::Console::WriteLine(exception->ToString());
        return -1;
    }
}

downloader.h 代码

#pragma once
#define CURL_STATICLIB
#include "curl/curl.h"
#include <string>
class Downloader final {
public:
    static size_t writeData(void* ptr, size_t size, size_t nmemb, FILE* stream) {
        size_t written = fwrite(ptr, size, nmemb, stream);
        return written;
    }

    static void download(System::Delegate^ progressFunction) {
        CURL* curl = curl_easy_init();
        if (curl) {
            const char* URL = "https://download.visualstudio.microsoft.com/download/pr/8e396c75-4d0d-41d3-aea8-848babc2736a/80b431456d8866ebe053eb8b81a168b3/ndp462-kb3151800-x86-x64-allos-enu.exe";
            const wchar_t* fileName = L"D:\\Backup\\Downloads\\dotnet462.exe";
            FILE* fp;
            _wfopen_s(&fp, fileName, L"wb");
            if (fp) {
                System::IntPtr progressFunctionPointer = System::Runtime::InteropServices::Marshal::GetFunctionPointerForDelegate(progressFunction);

                curl_easy_setopt(curl, CURLOPT_URL, URL);
                curl_easy_setopt(curl, CURLOPT_USERAGENT, "libcurl");
                curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeData);
                curl_easy_setopt(curl, CURLOPT_WRITEDATA, fp);
                curl_easy_setopt(curl, CURLOPT_NOPROGRESS, FALSE);
                curl_easy_setopt(curl, CURLOPT_PROGRESSFUNCTION, progressFunctionPointer);

                CURLcode res = curl_easy_perform(curl); // 此处触发程序崩溃

                if (res == CURLE_OK) {
                    System::Console::WriteLine("Download completed.");
                }
                else {
                    System::Console::WriteLine("Couldn't download file.");
                }

                std::fclose(fp);
            }
            else {
                System::Console::WriteLine("Couldn't open file.");
            }

            curl_easy_cleanup(curl);
        }
        else {
            System::Console::WriteLine("Couldn't initialize curl.");
        }
    }
};

MyForm.h 相关代码

public: delegate int DelegateProgressChanged(void* clientPointer, double totalToDownload, double nowDownloaded, double totalToUpload, double nowUploaded);

public: int progressChanged(void* clientPointer, double totalToDownload, double nowDownloaded, double totalToUpload, double nowUploaded) {
    double percentage = (nowDownloaded / totalToDownload) * 100;
    System::Console::WriteLine("I will print this before application crashes.");
    return 0;
}

public: System::Void startDownload() {
    Downloader::download(gcnew DelegateProgressChanged(this, &MyForm::progressChanged));
}

public: System::Void MyForm_Shown(System::Object^ sender, System::EventArgs^ e) {
    System::Threading::Tasks::Task::Run(gcnew System::Action(this, &MyForm::startDownload));
}

按照libcurl官方文档定义的委托和progressChanged函数签名完全匹配回调要求,直接传入非托管函数指针时代码可正常运行,但因为需要更新窗体上的System::Windows::Forms::ProgressBar控件展示进度,必须使用托管回调。
运行时程序会在指定目录创建不完整的下载文件,打印出*"I will print this before application crashes."*日志后,在curl_easy_perform调用处崩溃,Debug模式下也无法捕获异常,VS抛出的错误如下:

Exception thrown at 0x38AD0002 in DownloadProgressTest.exe: 0xC0000005: Access violation executing location 0x38AD0002.

Original Location: C:\Windows\SysWOW64\kernel32.dll

修复第一个崩溃问题后,更新进度条时出现新异常,错误信息如下:

percentage value is: -2147483648
percentage value is: -2147483648
percentage value is: -2147483648
System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation. ---> System.ArgumentOutOfRangeException: Value of '-2147483648' is not valid for 'Value'. 'Value' should be between 'minimum' and 'maximum'.
Parameter name: Value
   at System.Windows.Forms.ProgressBar.set_Value(Int32 value)
   at DownloadProgressTest.MyForm.BeginInvokeMeProgressChanged(Int32 percentage) in C:\Users\censored\source\repos\DownloadProgressTest\DownloadProgressTest\MyForm.h:line 94
   --- End of inner exception stack trace ---
   at System.RuntimeMethodHandle.InvokeMethod(Object target, Object[] arguments, Signature sig, Boolean constructor)
   at System.Reflection.RuntimeMethodInfo.UnsafeInvokeInternal(Object obj, Object[] parameters, Object[] arguments)
   at System.Delegate.DynamicInvokeImpl(Object[] args)
   at System.Windows.Forms.Control.InvokeMarshaledCallbackDo(ThreadMethodEntry tme)
   at System.Windows.Forms.Control.InvokeMarshaledCallbackHelper(Object obj)
   at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
   at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
   at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
   at System.Windows.Forms.Control.InvokeMarshaledCallback(ThreadMethodEntry tme)
   at System.Windows.Forms.Control.InvokeMarshaledCallbacks()
   at System.Windows.Forms.Control.WndProc(Message& m)
   at System.Windows.Forms.ScrollableControl.WndProc(Message& m)
   at System.Windows.Forms.Form.WndProc(Message& m)
   at System.Windows.Forms.Control.ControlNativeWindow.OnMessage(Message& m)
   at System.Windows.Forms.Control.ControlNativeWindow.WndProc(Message& m)
   at System.Windows.Forms.NativeWindow.DebuggableCallback(IntPtr hWnd, Int32 msg, IntPtr wparam, IntPtr lparam)
   at System.Windows.Forms.UnsafeNativeMethods.DispatchMessageW(MSG& msg)
   at System.Windows.Forms.Application.ComponentManager.System.Windows.Forms.UnsafeNativeMethods.IMsoComponentManager.FPushMessageLoop(IntPtr dwComponentID, Int32 reason, Int32 pvLoopData)
   at System.Windows.Forms.Application.ThreadContext.RunMessageLoopInner(Int32 reason, ApplicationContext context)
   at System.Windows.Forms.Application.ThreadContext.RunMessageLoop(Int32 reason, ApplicationContext context)
   at System.Windows.Forms.Application.Run(Form mainForm)
   at main(String[] args) in C:\Users\censored\source\repos\DownloadProgressTest\DownloadProgressTest\Program.cpp:line 9

因为进度函数运行在Task线程中,采用如下跨线程更新UI的代码:

public: System::Void BeginInvokeMeProgressChanged(int percentage) {
    this->progressBar1->Value = percentage;
    this->label1->Text = "Download progress: " + percentage + "%";
}
public: int progressChanged(void* clientPointer, double totalToDownload, double nowDownloaded, double totalToUpload, double nowUploaded) {
    int percentage = (int)((nowDownloaded / totalToDownload) * 100);
    System::Console::WriteLine("percentage value is: " + percentage);
    this->BeginInvoke(gcnew System::Action<int>(this, &MyForm::BeginInvokeMeProgressChanged), percentage);
    return 0;
}

现象为BeginInvoke似乎干扰了libcurl非托管进度函数中的percentage变量,导致数值异常。

问题原因与解决方案

第一个访问违例崩溃原因

两个核心问题导致第一次崩溃:

  • 委托被GC回收:在startDownload中创建的DelegateProgressChanged是临时局部变量,获取完函数指针后没有持有委托引用,GC会在回调触发前回收该委托,导致函数指针指向已释放的内存,触发执行地址访问违例。
  • 传参类型错误:CURLOPT_PROGRESSFUNCTION要求传入函数指针本身,直接传入IntPtr类型的托管对象时,C++/CLI的隐式转换没有正确取到指针地址,导致libcurl调用了错误的内存地址。

修复方法

  • 委托创建后必须持有引用直到下载完全结束,可以将委托存为窗体类的成员变量,或者在download函数中通过GCHandle固定委托不被回收。
  • 给curl_easy_setopt传进度回调时,取IntPtr的ToPointer()值传入,或者直接将委托转为对应类型的函数指针。

修改后的download函数关键部分示例:

// 固定委托引用,避免GC回收
GCHandle gch = GCHandle::Alloc(progressFunction);
try {
    IntPtr progressPtr = Marshal::GetFunctionPointerForDelegate(progressFunction);
    // 传入原生函数指针
    curl_easy_setopt(curl, CURLOPT_PROGRESSFUNCTION, progressPtr.ToPointer());
    curl_easy_perform(curl);
}
finally {
    gch.Free();
}

第二个进度值异常原因

两个问题导致百分比变成-2147483648(即0x80000000,int类型的最小值):

  • 回调调用约定不匹配:libcurl的回调默认使用cdecl调用约定,定义的托管委托没有显式指定调用约定时,默认是stdcall,调用栈不平衡会导致参数值读取错乱。
  • 边界值未处理:libcurl在刚开始下载还没获取到总文件大小时,totalToDownload的值是0,直接做除法会得到NaN/无穷值,转int就会变成异常值。

修复方法

  • 给委托显式加上cdecl调用约定标记:
    public: 
        [System::Runtime::InteropServices::UnmanagedFunctionPointerAttribute(System::Runtime::InteropServices::CallingConvention::Cdecl)]
        delegate int DelegateProgressChanged(void* clientPointer, double totalToDownload, double nowDownloaded, double totalToUpload, double nowUploaded);
    
  • 计算百分比前先做边界判断,总大小小于等于0时直接返回0,避免除零错误;同时把百分比值钳制在0-100区间,避免异常值传入UI控件:
    public: int progressChanged(void* clientPointer, double totalToDownload, double nowDownloaded, double totalToUpload, double nowUploaded) {
        int percentage = 0;
        if (totalToDownload > 0) {
            double pct = (nowDownloaded / totalToDownload) * 100.0;
            // 限制数值范围在0-100
            pct = max(0.0, min(100.0, pct));
            percentage = (int)pct;
        }
        System::Console::WriteLine("percentage value is: " + percentage);
        // 增加窗体句柄存在判断,避免关闭窗体时BeginInvoke抛错
        if (this->IsHandleCreated) {
            this->BeginInvoke(gcnew System::Action<int>(this, &MyForm::BeginInvokeMeProgressChanged), percentage);
        }
        return 0;
    }
    

内容的提问来源于stack exchange,提问作者Simple

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 03:20:04