踩坑记之特殊的头文件名

问题现象

windows 下编译 Share 项目时,编译器报各种莫名错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
[0/1 ?/sec] Re-running CMake...
-- Configuring done
-- Generating done
-- Build files have been written to: D:/Code/build-Share-Desktop_Qt_5_12_5_MSVC2017_32bit-Debug
[1/6 8.9/sec] Automatic MOC and UIC for target Share
[2/6 1.5/sec] Building CXX object CMakeFiles\Share.dir\Share_autogen\mocs_compilation.cpp.obj
FAILED: CMakeFiles/Share.dir/Share_autogen/mocs_compilation.cpp.obj
C:\PROGRA~2\MICROS~1\2017\COMMUN~1\VC\Tools\MSVC\1416~1.270\bin\HostX86\x86\cl.exe /nologo /TP -DQT_CORE_LIB -DQT_GUI_LIB -DQT_WIDGETS_LIB -I. -ID:\Code\Share -IShare_autogen\include -IC:\Qt\5.12.5\msvc2017\include -IC:\Qt\5.12.5\msvc2017\include\QtWidgets -IC:\Qt\5.12.5\msvc2017\include\QtGui -IC:\Qt\5.12.5\msvc2017\include\QtANGLE -IC:\Qt\5.12.5\msvc2017\include\QtCore -IC:\Qt\5.12.5\msvc2017\.\mkspecs\win32-msvc /DWIN32 /D_WINDOWS /W3 /GR /EHsc /showIncludes /MDd /Zi /Ob0 /Od /RTC1 /showIncludes /FoCMakeFiles\Share.dir\Share_autogen\mocs_compilation.cpp.obj /FdCMakeFiles\Share.dir\ /FS -c Share_autogen\mocs_compilation.cpp
D:\Code\Share\share.h(6): error C2504: “QObject”: 未定义基类
D:\Code\Share\share.h(7): error C2027: 使用了未定义类型“QString”
C:\Qt\5.12.5\msvc2017\include\QtCore/qchar.h(48): note: 参见“QString”的声明
d:\code\build-share-desktop_qt_5_12_5_msvc2017_32bit-debug\share_autogen\EWIEGA46WW/moc_share.cpp(85): error C2352: “QObject::qt_metacast”: 非静态成员函数的非法调用
c:\qt\5.12.5\msvc2017\include\qtcore\qobject.h(119): note: 参见“QObject::qt_metacast”的声明
d:\code\build-share-desktop_qt_5_12_5_msvc2017_32bit-debug\share_autogen\EWIEGA46WW/moc_share.cpp(90): error C2352: “QObject::qt_metacall”: 非静态成员函数的非法调用
c:\qt\5.12.5\msvc2017\include\qtcore\qobject.h(119): note: 参见“QObject::qt_metacall”的声明
[3/6 2.2/sec] Building CXX object CMakeFiles\Share.dir\mainwindow.cpp.obj
FAILED: CMakeFiles/Share.dir/mainwindow.cpp.obj
C:\PROGRA~2\MICROS~1\2017\COMMUN~1\VC\Tools\MSVC\1416~1.270\bin\HostX86\x86\cl.exe /nologo /TP -DQT_CORE_LIB -DQT_GUI_LIB -DQT_WIDGETS_LIB -I. -ID:\Code\Share -IShare_autogen\include -IC:\Qt\5.12.5\msvc2017\include -IC:\Qt\5.12.5\msvc2017\include\QtWidgets -IC:\Qt\5.12.5\msvc2017\include\QtGui -IC:\Qt\5.12.5\msvc2017\include\QtANGLE -IC:\Qt\5.12.5\msvc2017\include\QtCore -IC:\Qt\5.12.5\msvc2017\.\mkspecs\win32-msvc /DWIN32 /D_WINDOWS /W3 /GR /EHsc /showIncludes /MDd /Zi /Ob0 /Od /RTC1 /showIncludes /FoCMakeFiles\Share.dir\mainwindow.cpp.obj /FdCMakeFiles\Share.dir\ /FS -c D:\Code\Share\mainwindow.cpp
D:\Code\Share\share.h(6): error C2504: “QObject”: 未定义基类
D:\Code\Share\share.h(7): error C2027: 使用了未定义类型“QString”
C:\Qt\5.12.5\msvc2017\include\QtCore/qchar.h(48): note: 参见“QString”的声明
[4/6 3.0/sec] Building CXX object CMakeFiles\Share.dir\main.cpp.obj
FAILED: CMakeFiles/Share.dir/main.cpp.obj
C:\PROGRA~2\MICROS~1\2017\COMMUN~1\VC\Tools\MSVC\1416~1.270\bin\HostX86\x86\cl.exe /nologo /TP -DQT_CORE_LIB -DQT_GUI_LIB -DQT_WIDGETS_LIB -I. -ID:\Code\Share -IShare_autogen\include -IC:\Qt\5.12.5\msvc2017\include -IC:\Qt\5.12.5\msvc2017\include\QtWidgets -IC:\Qt\5.12.5\msvc2017\include\QtGui -IC:\Qt\5.12.5\msvc2017\include\QtANGLE -IC:\Qt\5.12.5\msvc2017\include\QtCore -IC:\Qt\5.12.5\msvc2017\.\mkspecs\win32-msvc /DWIN32 /D_WINDOWS /W3 /GR /EHsc /showIncludes /MDd /Zi /Ob0 /Od /RTC1 /showIncludes /FoCMakeFiles\Share.dir\main.cpp.obj /FdCMakeFiles\Share.dir\ /FS -c D:\Code\Share\main.cpp
D:\Code\Share\share.h(6): error C2504: “QObject”: 未定义基类
D:\Code\Share\share.h(7): error C2027: 使用了未定义类型“QString”
C:\Qt\5.12.5\msvc2017\include\QtCore/qchar.h(48): note: 参见“QString”的声明
[5/6 3.5/sec] Building CXX object CMakeFiles\Share.dir\share.cpp.obj
ninja: build stopped: subcommand failed.
15:08:49: 进程"C:\Users\87591\scoop\shims\cmake.exe"退出,退出代码 1 。
Error while building/deploying project Share (kit: Desktop Qt 5.12.5 MSVC2017 32bit)
When executing step "CMake Build"
15:08:49: Elapsed time: 00:02.

原因分析

首先新建一个Qt初始项目编译正常,排除开发环境问题。
接下来没有任何思路,只好逐步精简项目,发现移除share.hshare.cpp并移除其他文件中对share.h的包含后依然会报share.h中的错误,尝试修改share.h的名称找出哪个文件还依赖了该文件,发现改名后编译居然通过了。遂猜测该文件名可能有冲突。使用 everything 搜索share.h,发现同名文件"C:\Program Files (x86)\Windows Kits\10\Include\10.0.16299.0\ucrt\share.h",位于标准库搜索路径中,表明该文件属于标准库头文件。
至此,问题原因找到,我们的share.h文件覆盖了标准库中的share.h,导致原本应该包含标准库中share.h变成包含我们提供的share.h

解决方法

修改share.h的文件名为其他名称后解决。

但事情到这里还没彻底弄清楚,按照C++头文件包含规则,如果是双引号则优先在当前项目路径下查找头文件,如果是尖括号则优先从系统目录查找。这里已经可以确认示例项目中没有包含share.h文件,也就是说是程序依赖的库间接依赖了该头文件。而对于库来说,其本来目的肯定是要包含系统目录中的share.h,那么理论上编程习惯没有问题的话,应使用#inlcude <share.h>,这样会优先查找到系统库中的share.h,不会出问题。而根据目前现象来看,有两种猜测,要么是某个库中对share.h的依赖写成了#include "share.h",要么是 MSVC 编译器对""<>的处理存在问题。所以我们要找到到底是哪个文件包含了share.h
MSVC 提供了/showIncludes编译选项,用于打印头文件包含关系,我们用 Visual Studio 打开(用 Qt Creator 测试并没有打印),发现大致包含关系如下:

1
MainWindow.h > QMainWindow > ... > qbytearray.h > string > ... > xiosbase > share.h

所以直接包含share.h的是xiosbase这个文件,打开后发现其是通过#include <share.h>来包含的,所以猜测一不对,因此只可能是 MSVC 编译器对""<>的处理是存在问题的,但通过附件中的 TestInclude 项目又正常,猜测可能是在某些特定条件下才会触发该 bug,但到这里已经无法再继续追下去了,只能作罢。

JNI开发心得

前言

断断续续接触 JNI 开发也有三年多了,一直都想做些记录,但又觉得难以写出新意,网上随便一搜非常多的 JNI 入门教程。近期需要给同事做一个 JNI 开发的分享,因此通过本文做些记录。本文不是一个 JNI 入门教程,而是会侧重于 C++ 进行 Android JNI 开发的一些最佳实践以及辅助工具的介绍,帮助大家更好的学习 JNI。

在使用 JNI 作为中间层将 C++ 和 JAVA 粘合起来时,最关键的是实现两个语言间类与类的交互。如果有一个 JAVA 类,该类实现了某些功能,我们需要在 C++ 中访问该类的方法,通常我们会创建一个与之对应的 C++ 适配类,然后通过该 C++ 类来调用对应的 JAVA 类的方法,反之亦然。下面我们根据适配类调用实现类的方向分成 JAVA->C++ 和 C++->JAVA 两部分来介绍。

在库中开启Qt事件循环

示例代码

对于很多开发者来说,Qt仅仅是一个用于开发GUI程序的库,但实际上,Qt官方一直在致力于将Qt打造成一个跨平台的开发框架。Qt中提供了大量的基础设施和非GUI库可供我们在开发非GUI程序时所用,如网络相关的QNetwork模块,音视频相关的QMutilMedia模块,还有核心的QtCore模块。这些模块都已经相当成熟和完善,而且都具有优秀的跨平台性能。在和其他语言,如Java,python相比,库的相对不够丰富是C++的一个缺陷所在,除了标准库,boost等之外,Qt其实也是一个可以考虑的强大的补充。

如果我们只是打算使用诸如QString,QTL等基础设施,只需要链接QtCore这个库即可。然而Qt很多更为强大的特性和类,如信号槽,QTimer等,必须依赖Qt事件循环才能正常工作。如果使用我们开发的库的可执行程序恰好也是基于Qt开发,那么一切工作正常,然而,作为库的开发者,我们不能对库的使用者做出假设,因此如果我们需要使用信号槽等依赖Qt事件循环的特性时,我们就需要在库中开启Qt事件循序。

修改Windows系统环境变量

每个接触过Windows系统的开发人员都会接触到环境变量,相当一部分软件安装完成后必做的一步操作就是设置环境变量。Windows下的环境变量分为用户环境变量和系统环境变量。本文主要分享如何正确设置系统环境变量。

GUI界面

这是我们最常用的方式,即右键此电脑->属性->高级系统设置->环境变量,然后在系统变量栏中修改系统环境变量。该方法十分简单,但我们有时候可能需要通过程序来修改,又该怎么做呢?

环境变量

命令行

CMD中,我们可以通过set命令来设置环境变量,powershell中也可以通过$ENV:key=value的形式设置环境变量,但这只能影响当前命令行环境,命令行程序退出后就失效了。我们可以通过SetX来修改系统环境变量。官方描述如下:

在用户或系统环境创建或修改环境变量。能基于参数、注册表项或文件输入设置变量。

默认情况下,该命令是修改当前用户的的环境变量,如果我们需要修改系统环境变量,可以使用/M参数。但正如官方描述所说,该命令只能 创建或修改 环境变量,也就是说该命令无法用于删除一个环境变量。

注册表

根据微软的官方文档, Windows系统环境变量其实是保存在注册表中的,路径为HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\Environment,我们可以手动或通过代码修改注册表的方式来修改系统环境变量。

To programmatically add or modify system environment variables, add them to the HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\Environment registry key, then broadcast a WM_SETTINGCHANGE message with lParam set to the string “Environment”. This allows applications, such as the shell, to pick up your updates.

修改完成后我们需要广播一个消息类型为WM_SETTINGCHANGElParam参数为"Environment"的窗口消息,该消息将会通知应用程序获取我们的更新。经过测试我发现,目前Windows下的explorer.exe是会处理这个消息然后更新其环境变量。这也就解释了之前一直以来的一个困惑,就是在修改完系统环境变量后,新启动的程序有的可以获取到更新后的环境变量信息,而有的必须则不行。这其实是因为新进程是会继承父进程的环境变量,如果父进程处理了上述的窗口消息且主动更新了,那么子进程就会使用更新后的环境变量信息,例如通过explorer.exe(这是Windows的窗口管理器,包括开始菜单,桌面等都是由该程序管理)启动的程序。而如果某些程序没有处理这个窗口消息,那么其不会更新自己的环境变量,进而导致通过其启动的新进程也就是旧的环境变量信息。例如我使用的一款启动器软件就没有处理这个消息,导致如果我通过其启动的软件都是旧环境变量信息,只能重新启动该启动器或重启系统。

在发送这条窗口消息时,还有两个需要注意的问题。

  1. 如何将一个字符串设置给lParam参数?

    Windows下发送窗口消息的几种方法中,参数lParam都是是LPARAM类型,在Win10 x64系统下,其定义如下

    1
    2
    typedef LONG_PTR            LPARAM;
    typedef _W64 long LONG_PTR, *PLONG_PTR;

    也就是说lParam是一个整型,那如何将一个字符串赋值给整型呢?其实通过中间的LONG_PTR我们可以看出,这里Windows是用一个整型来存储字符串的地址。我们可以直接将字符串强转成一个整型。

    1
    SendMessage(HWND_BROADCAST, WM_SETTINGCHANGE, NULL, LPARAM("Environment"));
  2. 调用无效?

    上面的代码调用看上去已经满足文档上的要求了,但在实际测试时,在Win10 中文版系统上,发送该消息后,再通过explorer.exe启动新程序,新的程序还是旧的环境变量信息。难道文档错了么?这个问题困扰了我非常久,后来才发现这是由于Windows为了兼容不同的字符编码环境,绝大部分API都提供了三套方法,除了普通的形式外,还有一个以A结尾,一个以W的方法,其中A代表ASCII码,即窄字符串版本,W代表宽字符版本。普通形式的方法实际上会根据是否有定义__UNICODE__这个宏来define为以A结尾或以W结尾的方法。如SendMessage的定义如下:

    1
    2
    3
    4
    5
    #ifdef UNICODE
    #define SendMessage SendMessageW
    #else
    #define SendMessage SendMessageA
    #endif // !UNICODE

    在我的电脑环境下是有定义UNICODE宏的,因此SendMessage方法实际上是调用了SendMessageW方法,然而第四个参数"Environment"是一个ASCII字符串,这就导致参数和方法不匹配,进而导致该消息没有被正确处理。下面是几种可行的修改方式

    1
    2
    3
    SendMessageA(HWND_BROADCAST, WM_SETTINGCHANGE, NULL, LPARAM("Environment")); // 明确指定使用
    SendMessageW(HWND_BROADCAST, WM_SETTINGCHANGE, NULL, LPARAM(L"Environment")); // 明确指定使用宽字符版本
    SendMessage(HWND_BROADCAST, WM_SETTINGCHANGE, NULL, LPARAM(_T"Environment")); // _T是一个根据UNICODE宏调整的宏,分别会被定义为空或L

总结

下面是修改系统环境变量的几种方法的对比

方式 支持程序调用 支持手动修改 添加 修改 删除 即时生效
GUI界面
命令行
修改注册表并发送窗体消息

踩坑记之Jenkins无权限克隆的代码

踩坑

最近部门新采购了一台Mac Mini用作Jenkins自动构建服务器,由于之前搭建过Windows构建服务器,因此这次的Mac构建服务器的任务自然落在我的身上。开始一切都非常顺利,安装好必须的软件,配置好环境,但在正式上线,构建一个跨平台项目时,却发现macOS服务器始终无法从公司自建的Gitlab代码托管平台上克隆代码,而同一项目的其他平台(Windows和Linux)服务器却一切正常。macOS服务器的出错提示信息如下(部分信息用*代替)。

1
2
3
4
5
6
7
8
9
10
11
12
13
> git rev-parse --is-inside-work-tree # timeout=10
Setting origin to xxx.com:user/repo.git
> git config remote.origin.url xxx.com:path/repo.git # timeout=10
Fetching origin...
Fetching upstream changes from origin
> git --version # timeout=10
using GIT_SSH to set credentials **** private ssh key for git submodule
> git fetch --tags --progress origin +refs/heads/*:refs/remotes/origin/*
hudson.plugins.git.GitException: Command "git fetch --tags --progress origin +refs/heads/*:refs/remotes/origin/*" returned status code 128:
stdout:
stderr: Permission denied (publickey).
fatal: Could not read from remote repository.
Please make sure you have the correct access rights

由于这个项目是多个平台同时构建,各个平台的Jenkins项目配置是完全一致的,因此可以排除掉Jenkins配置问题,难道是macOS服务器配置错了?

网络分析实践之Dukto

最近在学习 wireshark 抓包进行网络分析,粗略地翻阅了几本相关的书籍后总感觉影响不够深刻,对于工具性的技术,最好的办法就是在实践中运用。刚好一直对于一直在用的一个工具Dukto的工作原理一直比较好奇,今天就来通过 wireshark 一探究竟。
Dukto是一款用于局域网文本/文件传输的小工具,自带局域网发现功能。本文主要分析Dukto如何实现局域网发现和数据传输。

踩坑记 - 消失的代码

示例代码

踩坑

最近在开发一个跨平台的 C++库时,在 Linux 平台运行非常正常,但在 Windows 下却怎么都没有预期的输出。为了简化问题,这里有一个存在同样问题的示例,可以看到示例中的代码非常简单,很显然预期输出应该是 5。

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>

using namespace std;

int main()
{
int a = 0;
// 赋一个非零的值
a = 5;
cout << a << endl;
return 0;
}

然而这个示例项目使用 VS 打开并编译运行后,输出结果却是 0。
VS运行输出

搭建Windows符号服务器

在Windows桌面软件开发完成并发布给用户使用后,难免会遇到崩溃问题。通过CrashDump文件来分析崩溃问题是一个行之有效的途径。我们可以在程序崩溃时在客户电脑上生成CrashDump文件并收集这些文件。但要打开并分析CrashDump文件,我们还需要产生CrashDump文件的可执行文件(后续称为PE文件,包括exedll文件)以及编译该PE文件时产生的符号文件(PDB)。比较基础的做法一般是在从客户电脑上拿到CrashDump文件和可执行文件后,再根据可执行文件的版本,编译时间等信息从构建服务器等地方找到相对应的PDB文件。这样带来的一个问题是我们需要手动做文件的匹配,由于PE文件,PDB文件和CrashDump文件三者是严格匹配的,即使是相同的代码,每次编译产生的文件都不一样,因此手动匹配稍有差错就会找不到正确的符号,导致无法分析CrashDump。为了解决这一问题,一个更好的方法是搭建一个符号服务器,在符号服务器上保存每次自动构建出的PE文件和PDB文件,让调试器根据CrashDump文件自动识别和匹配。下面将会介绍如何搭建一个私有的符号服务器。

加快Qt程序的编译速度

在项目开发中,随着产品的更新迭代,项目会变得越来越大。项目的增大就会导致编译速度也会变得越来越慢。本人负责的一个项目的代码量如下

1
2
3
4
5
6
7
8
9
10
github.com/AlDanial/cloc v 1.80  T=0.07 s (2139.2 files/s, 210543.4 lines/s)
-------------------------------------------------------------------------------
Language files blank comment code
-------------------------------------------------------------------------------
C++ 73 1560 453 7993
C/C++ Header 78 1167 865 2919
Markdown 1 1 0 2
-------------------------------------------------------------------------------
SUM: 152 2728 1318 10914
-------------------------------------------------------------------------------

在不同的环境下全新构建一次耗费时间(单位:秒)如下

Qt Creator Visual Studio 2017 命令行
本地 81 241 246
服务器 - - 183

Qt从0到1之机制篇 - Qt样式表

本节示例代码

在Web前端开发中,有一个很重要的部分是CSS,用于修饰Html中的控件,如按钮,输入框等元素。CSS的出现分离了交互逻辑和视觉效果的开发,极大地方便了前端开发。因此这套设计理念被很多其他前端开发框架所借鉴,如Qt,Gtk,JavaFX等。在Qt中,默认的控件样式是平台相关的原始风格,很多时候难以满足视觉设计的要求,因此我们可以借助Qt中的CSSQt Style Sheet(一般简称为QSS)来对控件进行修饰美化,定制化出我们想要的效果。

|