wxWidgets C++程序执行文件下载功能时崩溃问题求助
问题现象
开发基于wxWidgets的C++下载应用,使用cURL处理下载请求,点击安装按钮后程序立即关闭,调试器捕获到如下异常:
异常抛出在 0x00007FFC285301E8 (wxbase32ud_vc_custom.dll) 中的 WindowsInstaller.exe: 0xC0000005: 写入位置 0x00000080FDFDFDFD 时发生访问违例。
核心错误分析
WriteCallback类型转换完全错误
代码中设置CURLOPT_WRITEDATA为文件指针file,但WriteCallback里强行把userp转换成wxThreadEvent*,直接导致对非法内存地址的写入操作,触发访问违例。同时WriteCallback的设计完全偏离职责——它应该将cURL获取到的字节写入本地文件,而非发送UI事件。进度捕获方式错误
要实现下载进度更新,不能篡改WriteCallback,应使用cURL专门的进度回调CURLOPT_XFERINFOFUNCTION(旧版本可用CURLOPT_PROGRESSFUNCTION)。线程悬空引用问题
下载线程的lambda捕获了局部变量downloader的引用(&downloader),但downloader是OnInstallButtonClick函数内的局部对象,函数执行完毕后该对象会被销毁,线程后续访问该引用会触发未定义行为。缺少cURL全局初始化
未调用curl_global_init(CURL_GLOBAL_ALL)初始化cURL全局环境,导致cURL无法正常工作。未绑定线程事件处理函数
定义了ID_UPDATE_PROGRESS等线程事件,但NewFrame中没有绑定对应的处理函数,事件无法被处理。
修复后的完整代码
#include <wx/wx.h> #include <iostream> #include <fstream> #include <curl/curl.h> #include <thread> #include <functional> class FileDownloader { public: using ProgressCallback = std::function<void(double)>; FileDownloader(ProgressCallback progressCb) : m_progressCb(std::move(progressCb)) {} ~FileDownloader() = default; static size_t WriteCallback(void* contents, size_t size, size_t nmemb, void* userp) { size_t totalSize = size * nmemb; FILE* file = static_cast<FILE*>(userp); return fwrite(contents, size, nmemb, file); } static int ProgressCallback(void* clientp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow) { (void)ultotal; (void)ulnow; auto progressCb = static_cast<ProgressCallback*>(clientp); if (dltotal > 0 && progressCb) { double progress = static_cast<double>(dlnow) / dltotal * 100.0; (*progressCb)(progress); } return 0; } bool DownloadFile(const std::string& url, const std::string& destination) { CURL* curl = curl_easy_init(); if (!curl) return false; FILE* file = fopen(destination.c_str(), "wb"); if (!file) { curl_easy_cleanup(curl); return false; } curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback); curl_easy_setopt(curl, CURLOPT_WRITEDATA, file); curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0L); curl_easy_setopt(curl, CURLOPT_XFERINFOFUNCTION, ProgressCallback); curl_easy_setopt(curl, CURLOPT_XFERINFODATA, &m_progressCb); // 禁用SSL验证(仅测试用,生产环境需开启) curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L); curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L); CURLcode res = curl_easy_perform(curl); fclose(file); curl_easy_cleanup(curl); return res == CURLE_OK; } private: ProgressCallback m_progressCb; }; class WindowsInstaller : public wxApp { public: bool OnInit() override; int OnExit() override; }; wxIMPLEMENT_APP(WindowsInstaller); class NewFrame : public wxFrame { public: NewFrame(); private: void OnHello(wxCommandEvent& event); void OnExit(wxCommandEvent& event); void OnAbout(wxCommandEvent& event); void OnInstallButtonClick(wxCommandEvent& event); void OnSetButtonClick(wxCommandEvent& event); void OnCheckBoxClicked(wxCommandEvent& event); void OnUpdateProgress(wxThreadEvent& event); void OnDownloadComplete(wxThreadEvent& event); void OnDownloadError(wxThreadEvent& event); void OnEnableInstallButton(wxThreadEvent& event); wxButton* install_button; wxButton* set_location; wxTextCtrl* textCtrl; wxCheckBox* accept_checkbox; wxGauge* gauge; }; bool WindowsInstaller::OnInit() { // 初始化cURL全局环境 curl_global_init(CURL_GLOBAL_ALL); NewFrame* frame = new NewFrame(); frame->Show(); return true; } int WindowsInstaller::OnExit() { // 清理cURL全局环境 curl_global_cleanup(); return wxApp::OnExit(); } enum { ID_Hello = 1, ID_Install_Button = 2, ID_Set_Location = 3, ID_Accept_CheckBox = 4, ID_UPDATE_PROGRESS = 5, ID_DOWNLOAD_COMPLETE = 6, ID_DOWNLOAD_ERROR = 7, ID_ENABLE_INSTALL_BUTTON = 8 }; NewFrame::NewFrame() : wxFrame(nullptr, wxID_ANY, "Hello World", wxDefaultPosition, wxSize(700, 500), wxDEFAULT_FRAME_STYLE & ~(wxRESIZE_BORDER | wxMAXIMIZE_BOX)) { wxMenu* menuFile = new wxMenu; menuFile->Append(ID_Hello, "&Hello...\tCtrl+H", "Help string shown in status bar for this menu item"); menuFile->AppendSeparator(); menuFile->Append(wxID_EXIT); wxMenu* menuHelp = new wxMenu; menuHelp->Append(wxID_ABOUT); wxMenuBar* menuBar = new wxMenuBar; menuBar->Append(menuFile, "&File"); menuBar->Append(menuHelp, "&Help"); SetMenuBar(menuBar); CreateStatusBar(); SetStatusText("Welcome to wxWidgets!"); wxBoxSizer* mainSizer = new wxBoxSizer(wxVERTICAL); textCtrl = new wxTextCtrl(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxTE_MULTILINE | wxTE_READONLY | wxHSCROLL); textCtrl->SetBackgroundColour(*wxWHITE); textCtrl->SetForegroundColour(*wxBLACK); textCtrl->SetFont(wxFont(wxFontInfo(10))); wxString licenseText = "Copyright 2023 Author\n" "Permission is hereby granted, free of charge, to any person obtaining a copy of this software " "and associated documentation files(the “Software”), to deal in the Software without restriction, " "including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, " "and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do " "so, subject to the following conditions:\n" "The above copyright notice and this permission notice shall be included in all copies or substantial " "portions of the Software.\n" "THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR " "IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS " "FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE AUTHORS " "OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, " "WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN " "CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE."; textCtrl->SetValue(licenseText); mainSizer->Add(textCtrl, 1, wxEXPAND | wxALL, 20); accept_checkbox = new wxCheckBox(this, ID_Accept_CheckBox, "I accept the license agreement"); mainSizer->Add(accept_checkbox, 0, wxALIGN_LEFT | wxALL, 10); gauge = new wxGauge(this, wxID_ANY, 100, wxDefaultPosition, wxDefaultSize, wxGA_HORIZONTAL); mainSizer->Add(gauge, 0, wxEXPAND | wxALL, 20); install_button = new wxButton(this, ID_Install_Button, "Install"); set_location = new wxButton(this, ID_Set_Location, "Set Location"); install_button->Enable(false); mainSizer->AddStretchSpacer(); mainSizer->Add(install_button, 0, wxALIGN_CENTER | wxALL, 10); mainSizer->Add(set_location, 0, wxALIGN_CENTER | wxALL, 5); SetSizerAndFit(mainSizer); // 绑定基础事件 Bind(wxEVT_MENU, &NewFrame::OnHello, this, ID_Hello); Bind(wxEVT_MENU, &NewFrame::OnAbout, this, wxID_ABOUT); Bind(wxEVT_MENU, &NewFrame::OnExit, this, wxID_EXIT); Bind(wxEVT_BUTTON, &NewFrame::OnInstallButtonClick, this, ID_Install_Button); Bind(wxEVT_BUTTON, &NewFrame::OnSetButtonClick, this, ID_Set_Location); Bind(wxEVT_CHECKBOX, &NewFrame::OnCheckBoxClicked, this, ID_Accept_CheckBox); // 绑定线程事件处理函数 Bind(wxEVT_THREAD, &NewFrame::OnUpdateProgress, this, ID_UPDATE_PROGRESS); Bind(wxEVT_THREAD, &NewFrame::OnDownloadComplete, this, ID_DOWNLOAD_COMPLETE); Bind(wxEVT_THREAD, &NewFrame::OnDownloadError, this, ID_DOWNLOAD_ERROR); Bind(wxEVT_THREAD, &NewFrame::OnEnableInstallButton, this, ID_ENABLE_INSTALL_BUTTON); SetInitialSize(wxSize(700, 500)); } void NewFrame::OnExit(wxCommandEvent& event) { Close(true); } void NewFrame::OnAbout(wxCommandEvent& event) { wxMessageBox("This is a wxWidgets Hello World example", "About Hello World", wxOK | wxICON_INFORMATION); } void NewFrame::OnHello(wxCommandEvent& event) { wxLogMessage("Hello world from wxWidgets!"); } void NewFrame::OnInstallButtonClick(wxCommandEvent& event) { if (!accept_checkbox->IsChecked()) { wxMessageBox("Please accept the license agreement.", "Error", wxOK | wxICON_ERROR); return; } wxString url = "https://releases.ubuntu.com/22.04.2/ubuntu-22.04.2-desktop-amd64.iso"; wxString destination = "F:/ubuntu-22.04.2-desktop-amd64.iso"; install_button->Disable(); gauge->SetValue(0); // 按值捕获变量,避免悬空引用 std::thread downloadThread([this, url, destination]() { auto progressCb = [this](double progress) { wxThreadEvent* progressEvent = new wxThreadEvent(wxEVT_THREAD, ID_UPDATE_PROGRESS); progressEvent->SetInt(static_cast<int>(progress)); wxQueueEvent(this, progressEvent); }; FileDownloader downloader(std::move(progressCb)); bool success = downloader.DownloadFile(url.ToStdString(), destination.ToStdString()); if (success) { wxThreadEvent* completeEvent = new wxThreadEvent(wxEVT_THREAD, ID_DOWNLOAD_COMPLETE); wxQueueEvent(this, completeEvent); } else { wxThreadEvent* errorEvent = new wxThreadEvent(wxEVT_THREAD, ID_DOWNLOAD_ERROR); wxQueueEvent(this, errorEvent); } wxThreadEvent* enableEvent = new wxThreadEvent(wxEVT_THREAD, ID_ENABLE_INSTALL_BUTTON); wxQueueEvent(this, enableEvent); }); downloadThread.detach(); } void NewFrame::OnSetButtonClick(wxCommandEvent& event) { wxMessageBox("Button clicked!", "Button", wxOK | wxICON_INFORMATION); } void NewFrame::OnCheckBoxClicked(wxCommandEvent& event) { install_button->Enable(accept_checkbox->IsChecked()); } void NewFrame::OnUpdateProgress(wxThreadEvent& event) { int progress = event.GetInt(); gauge->SetValue(progress); SetStatusText(wxString::Format("Downloading: %d%%", progress)); } void NewFrame::OnDownloadComplete(wxThreadEvent& event) { wxMessageBox("Download completed successfully!", "Success", wxOK | wxICON_INFORMATION); SetStatusText("Download completed"); } void NewFrame::OnDownloadError(wxThreadEvent& event) { wxMessageBox("Download failed!", "Error", wxOK | wxICON_ERROR); SetStatusText("Download failed"); } void NewFrame::OnEnableInstallButton(wxThreadEvent& event) { install_button->Enable(true); }
关键修改说明
- 修复WriteCallback:恢复其写入文件的本职功能,正确转换
userp为FILE*并调用fwrite。 - 添加进度回调:使用
CURLOPT_XFERINFOFUNCTION捕获下载进度,通过回调函数发送UI事件更新进度条。 - 解决线程悬空引用:线程lambda按值捕获必要变量,避免引用局部对象。
- 初始化cURL全局环境:在
OnInit中调用curl_global_init,OnExit中调用curl_global_cleanup。 - 绑定线程事件处理函数:新增
OnUpdateProgress等函数,处理进度更新、完成、错误等事件。 - 优化事件发送:直接向当前
NewFrame实例发送事件,而非依赖wxTheApp->GetTopWindow(),更可靠。
内容的提问来源于stack exchange,提问作者Ethan James

