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
相关产品推荐
相关产品推荐

