这个话题并不是一个新的话题,网上有前辈做过详细的描述(Mixing Objective-C, C++ and Objective-C++: an Updated Summary)。之所以做为一篇文章,是因为在实际项目中用到了这种混合,加以记录。

首先需要澄清的是,Objective-C是strict superset of C。C++是基于C语言的面向对象的扩充,但不是strict superset of C。Objective-C++是Objective-C的扩展,使得Objective-C可以链接C++代码。通常情况下,要使用Objective-C++,需要将.m源文件改成.mm源文件。

其次需要澄清的是,Objective-C代码跟C++代码是不能混合使用的,也就是说,Objective-C的头文件中是看不到C++的头文件的。当然,C++的头文件是看不到Objective-C的头文件的。只有Objective-C++文件(也就是.mm文件)可以看到Objective-C的头文件和C++的头文件。

正因为Objective-C和C++互相不能见到对方的头文件,那么就意味着,现有的C++的代码库,想要在Objective-C中使用,是需要封装的,通常,这种封装是借助于Objective-C++写成的Wrapper.

另一方面,Wrapper本身也有技巧,那就是使用Objective-C的class extension特性(或者使用Pointer to implementation设计模式),避免将C++头文件暴露给Objective-C(否则无法编译通过)。具体细节可以参考文章开头的链接。

背景

在使用Qt给MacOS上的截图工具实现自动选中窗口区域需求的时候,原理无非就是获得桌面上可见的所有窗口的坐标以及尺寸,再将这些数据用一个结构体封装起来,按照在桌面的Z序进行排序,最后对鼠标的移动事件进行判断,按照Z序去对鼠标是否在某个窗口的矩形范围内进行判断,直到找到在范围内的矩形,这就是我们要自动选中的矩形了,如果将所有窗口都遍历完了还没发现鼠标正在某个矩形内的话,就不进行自动选中操作。

在Windows下我们可以很方便的使用EnumWindows这个Win32 API去获得桌面上所有窗口的坐标和尺寸,Win32 API可以由C++很方便的调用,而在MacOS上,单纯的使用C++仿佛无法获得我需要的这些数据,于是只好使用Apple的Objective-C提供的接口去获得这些数据。

通过在苹果开发者文档找到了MacOS上很多获得窗口信息的接口,使用CGWindowListCopyWindowInfo就能得到每个窗口的具体数据,接下来我们就要开始Qt与Objective-C混合编程了。

Objective-C是一种在C的基础上加入面向对象特性扩充而成的编程语言,通常称为jObC和较少用的 Objective C或ObjC。在一定程度上,可以把 Objective-C看成是ANSI版本C语言的一个超集,它支持相同的C语言基本语法,同时它还扩展了标准的 ANSI C语言的语法,包括定义类、方法和属性。当然还有其他一些结构的完善和拓展,如类别(Category)的出现。

所以Objective-c是可以直接调用C语言的,那么能否直接调用C++呢?答案是肯定的。

Objective-C源文件介绍

首先我要说一下Objective-C的源文件,后缀是.m或.mm,在.mm文件里,可以直接使用C++代码。所以,我们要混合Qt代码与Objective-C代码,就需要在Qt项目里加入mm文件。

而要混合Objective-C代码,需要更改一下pro文件,添加mm文件,如果有用到MacOS的API的话,则可能还需要添加MacOS的Framework。

添加源文件,需要在.pro中使用OBJECTIVE_SOURCES这个变量,如下所示:

OBJECTIVE_SOURCE += \
Getallvisiblewndpos.mm

添加头文件,需要在.pro中使用OBJECTIVE_HEADERS这个变量,如下所示:

OBJECTIVE_HEADERS += \
getallvisiblewndpos.h

添加Framework,需要在.pro中使用LIBS这个变量,如下所示:

LIBS += -framework CoreGraphics
LIBS += -framework CoreFoundation

混合代码

要使用MacOS提供的框架,需要在.mm文件内包含相关的头文件,又有几部分工作要做。一个是在.pro文件里加入Framework路径,使用LIBS变量即可,不多说了。另外一点是在mm文件内包含Objective-C的头文件,与C++头文件一个理儿,不过使用Objective-C的头文件要使用#import,而使用C++的头文件要使用#include,且所有的#include要写在#import的上面。

需要注意的是,凡是出现了Objective-C源代码和头文件的文件都需要将拓展名改成.mm,只有这样编译器在编译的时候才会既认识Objective-C代码,又认识C++代码。

注意事项

当遇到链接失败的问题时,如下图所示:

通常都是因为只引入了头文件而没有引入对应的Framework,通过在苹果开发者文档或者在Xcode中查找对应的Framework并添加到.pro文件中加以解决。

附赠Qt下Objective-C获得MacOS下所有窗口坐标、尺寸、Z序的方式:

//getAllVisibleWndPos.h
#ifndef GETALLVISIBLEWNDPOS_H
#define GETALLVISIBLEWNDPOS_H

#include <QRect>
#include <QList>
#include <vector>

#import <Foundation/Foundation.h>
#import <Cocoa/Cocoa.h>
#import <CoreGraphics/CGWindow.h>
#import <CoreFoundation/CFArray.h>

struct WND_INFO{
	int layer;//Z序
	int index;//序号
	QRect pos;
};

bool CompareWNDINFO(const WND_INFO& A , const WND_INFO& B);
QList<WND_INFO> GetAllVisibleWndPos();

#endif // GETALLVISIBLEWNDPOS_H
//getAllVisibleWndPos.mm
#import <getallvisiblewndpos.h>

bool CompareWNDINFO(const WND_INFO& A , const WND_INFO& B)
{
	if(A.layer == B.layer)
	{
		return A.index < B.index;
	}
	if(A.layer > B.layer && B.layer > 0)
	{
		return true;
	}
	if(A.layer < B.layer && A.layer > 0)
	{
		return false;
	}
	return A.layer == 0;
}

QList<WND_INFO> GetAllVisibleWndPos()
{
	std::vector<WND_INFO> vec;
	CFArrayRef windowList = CGWindowListCopyWindowInfo(kCGWindowListOptionOnScreenOnly | kCGWindowListExcludeDesktopElements , kCGNullWindowID);
	CFIndex cnt = CFArrayGetCount(windowList);
	for (CFIndex i = 0; i < cnt ; i++)
	{
		NSDictionary *dict = (NSDictionary* )CFArrayGetValueAtIndex(windowList , i);
		int layer = 0;
		CFNumberRef numberRef = (__bridge CFNumberRef) dict[@"kCGWindowLayer"];
		CFNumberGetValue(numberRef , kCFNumberSInt32Type , &layer);
		if(layer < 0)
			continue;
		CGRect windowRect;
		CGRectMakeWithDictionaryRepresentation((__bridge CFDictionaryRef)(dict[@"kCGWindowBounds"]) , &windowRect);
		
		QRectF pos;
		pos.setRect(windowRect.origin.x , windowRect.origin.y , windowRect.size.width , windowRect.size,height);
		
		WND_INFO info;
		info.layer = layer;
		info.pos.setRect(pos.x() , pos.y() , pos.width() , pos.height());
		info.index = i;
		
		vec.push_back(info);
	}
	
	std::sort(vec.begin() , vec.end() , CompareWNDINFO);
	QList<WND_INFO> list;
	for(int i = 0 ; i < vec.size() ; i++)
	{
		list.push_back(vec[i]);
	}
	return list;
}

引言

为何redis中大量使用的是SDS,而不是传统的C语言字符串表示法存储字符串?到底什么是SDS?为什么要使用SDS?其相对于传统的C语言字符串有何优点?本文会针对以上几点做一个详细的分析,帮助大家以及自己更好的理解redis中的简单动态字符串

介绍SDS之前先简单介绍一下C语言中的传统字符串表示法

C语言字符串(C字符串)

始终以空字符结尾的字符数组,c语言为其封装了大量的函数库操作API

C字符串特点
  • 字符数组存储
  • 以空字符结尾,除了末尾之外,字符串里面不可以包含空字符
  • 由于采用字符数组,导致所存储的字符必须符合某种编码(如ASCII)
  • 获取C字符串长度必须遍历整个字符串,并对遇到的每个字符计数,直到遇到空字符为止,时间复杂度为O(N)

那么基于以上特点,C字符串是否能够满足我们Redis高效,安全的需求呢?显然是不能的,单是一个获取字符串长度就需要遍历整个字符串数组了,必须重新定义一种新的结构以支持Redis中对于频繁高效操作字符串的要求。


SDS 简单动态字符串

基于以上C字符串的缺陷,Redis自己构建了一种名为简单动态字符串的抽象类型,简称SDS。

SDS结构
struct sdshdr {

    // buf数组已经使用字节数量,即SDS字符串长度
    int len;

    // 记录buf数组中未使用字节的数量
    int free;

    // 字节数组,用于保存二进制数据
    char buf[];
}

可以看出,SDS定义的结构中,增加了一个int类型的len属性用于记录SDS所保存的字符串长度,一个free属性用于记录数组中未使用的字节数量。

SDS特点
  • 常数复杂度获取字符串长度

Redis中利用SDS字符串的len属性可以直接获取到所保存的字符串的长
度,直接将获取字符串长度所需的复杂度从C字符串的O(N)降低到了O(1)。


  • 减少修改字符串时导致的内存重新分配次数

基于前面介绍的C字符串的特性,我们知道对于一个包含了N个字符的C字符串来说,其底层实现总是N+1个字符长的数组(额外一个空字符结尾),那么如果这个时候需要对字符串进行修改,程序就需要提前对这个C字符串数组进行一次内存重分配(可能是扩展或者释放),而内存重分配就意味着是一个耗时的操作。

Redis巧妙的使用了SDS避免了C字符串的缺陷,在SDS中,buf数组的长度不一定就是字符串的字符数量加一,buf数组里面可以包含未使用的字节,而这些未使用的字节由free属性记录。

SDS采用了空间预分配策略避免C字符串每一次修改时都需要进行内存重分配的耗时操作,将内存重分配从原来的每修改N次就分配N次降低到了修改N次最多分配N次。

字符串扩展:

首先检查SDS未使用空间即free属性是否够用,如果够用,则会直接使用未使用空间,而无须进行内存重分配

空间预分配

所谓预分配就是额外多分配一部分空间给SDS,并不是仅仅只分配刚好够字符串扩展修改后的空间。

分配策略:

  1. 若SDS修改后,其长度(len的属性)小于1MB,那么程序会分配和修改后的len属性同样大小的未使用空间
  2. 若修改后,其长度大于1MB,那么程序只会分配固定1MB的未使用空间,不会再多分配了,考虑内存的成本因素

字符串缩短:

针对SDS字符串的缩短场景,SDS并不会立即释放多余出来的内存空间,而是将这部分内存空间记录再其free属性中,当SDS字符串进行扩展时,这部分未使用的内存空间就能直接用,而不需要进行内存重分配,这就是SDS的惰性空间释放


  • 杜绝缓冲区溢出

在C字符串的拼接操作过程中,例如某程序员操作S1拼接S2,由于程序员忘记了给需要拼接的字符串S1分配足够的内存空间(到底需要分配多少内存呢?哈哈,当然需要遍历S2的字符数组才能知道S2的长度是多少,因为C字符串不记录自身的长度),那么拼接的时候就会导致缓冲区溢出的可能性。

针对以上情况,SDS的空间分配策略可以完全杜绝这种情况,因为当SDS 的API对SDS进行修改时,API会首先检查SDS的未使用空间是否足够,不够的化会进行内存重分配以扩展空间至足够修改所需的大小,然后再执行实际的修改操作,这样就可以达到杜绝缓冲区溢出的可能。


  • 让Redis保存更多类型的数据成为可能

由于C字符串中保存的字符必须符合某种编码格式(如ASCII),这就限制了C字符串只能保存文本数据,而不能保存箱视频,音频,压缩文件这种的二进制数据。

另一个限制是C字符串中间不能包含空字符,否则中间的空字符会被认为是整个字符串的结尾,而导致后面的部分字符被忽略掉。

SDS的API被设计成了二进制安全的,以处理二进制的方式来处理SDS中存放再buf数组中的数据,原样存取,这就是为什么在SDS的结构中采用的是字节数组,而不是C字符串中的字符数组

这样的二进制安全的SDS,使得Redis不仅可以保存文本,还可以保存任意格式的二进制数据。


  • 兼容部分C字符串API

由于C字符串本身具有大量操作API,SDS如果可以利用一部分C字符串的API那样就不用重复发明轮子了,所以Redis中的SDS遵循C字符串以空字符结尾的惯例,在SDS的API中,总会将SDS保存的数据末尾设置未空字符,在分配buf数组时也总会多分配一个字节来保存这个空字符,这样SDS就可以重用一部分C字符串库的API。


C字符串与SDS对比

对比点C字符串SDS
获取字符串长度时间复杂度O(N)O(1)
API安全性不安全,可能造成缓冲区溢出安全,不会造成溢出
字符串修改N次需要几次内存重分配N次至多N次
能够保存数据类型只能保存文本文本或二进制
对于C语言中字符串API的使用范围所有一部分

总结

Redis中采用SDS替代C语言中传统字符串表示法,提升了获取字符串长度的效率,扩大了能够保存数据的类型范围,以及降低了每次修改字符串时候的内存重分配次数,甚至规避了在操作C字符串中可能出现的缓冲区内存溢出的可能性,从而为Redis中字符串操作的安全,高效提供了保障。

编译环境:VS2019 + Win10 + cmake-gui-3.8.0 + cef_binary_3.2623.1401.gb90a3be_windows32

最后一个兼容Windows XP的CEF(2623)的下载地址:https://pan.baidu.com/s/1UoWt8Ffs_YPBCmYlHbipLg

提取码:x7ym

1、解压 cef_binary_3.2623.1401.gb90a3be_windows32 后,目录如下:

2、下载cmake-gui

下载地址:https://pan.baidu.com/s/1KdOaZXWX9gy7yVKVbJdpgA

提取码:ptnu

下载好cmake-gui并安装好之后打开cmake-gui.exe,设置如下:

Where is the source code : cef_binary_3.2623.1401.gb90a3be_windows32解压后的路径

where to build the binaries : cef_binary_3.2623.1401.gb90a3be_windows32解压后的路径

Configure: 选择你电脑上装有的VS的编译器的版本,如果选择了电脑本地并没有的VS编译器版本,会遇到如下情况:

用cmake生成编译工程时候报这样的错误,原因是配置错误导致cmake找不到对应的编译器,于是通过File->Delete cache清理配置,重新通过Configure更换你电脑上装有的VS的编译器的版本即可。

当出现Configuring done的时候点击Generate按钮即可生成对应版本的VS sln解决方案,使用VS打开生成解决方案即可。

VS2015打开cef.sln然后直接编译即可生成libcef_dll_wrapper.lib文件了,如下图项目cefsimple项目和cefclient项目会失败,这个并不影响生成我需要的libcef_dll_wrapper.lib,我就不解决了。

在这里还有一个坑就是这个工具最多只支持到VS2017,由于我的电脑上装了VS2013和VS2019,于是我选择了VS2013的配置并成功编译出了libcef_dll_warpper.lib,但在导入CEF浏览器实际项目调用的时候报了如下错误:error LNK2038: 检测到“_MSC_VER”的不匹配项问题。

_MSC_VER这个相当于做了宏的检测  _MSC_VER 定义编译器的版本。下面是一些编译器版本的_MSC_VER值:
MS VC++ 14.0 _MSC_VER = 1900 vs2015
MS VC++ 12.0 _MSC_VER = 1800 vs2013的编译器他的平台是v120
MS VC++ 11.0 _MSC_VER = 1700 vs2012的编译器他的平台是v110
MS VC++ 10.0 _MSC_VER = 1600 Visual C++ 2010
MS VC++ 9.0 _MSC_VER = 1500 Visual C++ 2008
MS VC++ 8.0 _MSC_VER = 1400 Visual C++ 2005
MS VC++ 7.1 _MSC_VER = 1310
MS VC++ 7.0 _MSC_VER = 1300
MS VC++ 6.0 _MSC_VER = 1200
MS VC++ 5.0 _MSC_VER = 1100

error LNK2038: 检测到“_MSC_VER”的不匹配项: 值“1800”不匹配值“1700”(main.obj 中)
原因:由于你使用了vs2012,工作集选择了更高的1800也就是vs2013的,致使msvc不兼容!
方法:在项目(解决方案资源管理器或者属性管理器里都行)右键属性-配置属性-常规中,平台工具集选用为合适平台即可,比如上面的就是要选择成2012的 v11版本,注意光选了还没有用,还要应用。
注意一个工程里面会有几个解决方案的时候,需要给每个解决方案都更改一遍,最后重新编译即可。

最近在通过OpenHardWareMonitorLib来获得一些CPU和GPU的信息,采用了c++调用c#dll的方法,由于只能传递基本数据类型,所以动态数组考虑到使用String来传递回C++并进行字符串分割,在.net中string是需要用gcnew进行初始化,先来看看gcnew和普通的new的区别:

C++/CLI中使用gcnew关键字表示在托管堆上分配内存,并且为了与以前的指针区分,用^来替换* ,就语义上来说他们的区别大致如下:

1.     gcnew返回的是一个句柄(Handle),而new返回的是实际的内存地址. 
2.     gcnew创建的对象由虚拟机托管,而new创建的对象必须自己来管理和释放.
暂时没有很深入的去理解这些区别。因为需要在c++的控制代码中对c#产生的String^变量进行写出,而默认的文件写出是string类型的,因此需要进行转换。查阅资料发现有人总结了一下较为简单的转换方式:

1:std::string转String^:std::string stdstr=””;
String^ str = marshal_as<String^>(stdstr);

2:String^转std::string:
String^ str= gcnew String();
std::string stdstr = marshal_as(str->ToString());

3:CString转Sting^:
CString cstr=””;
String^ str = marshal_as(cstr.GetBuffer());
cstr.ReleaseBuffer();

4:String^转CString:
String^ str;
CString cstr(str);

1、打开Visual Studio,新建一个C#的Class Library项目(这里选择的是.Net Framework 4),项目名为CSharpDll。

2、由于默认没有引入Forms等UI库,先在reference中添加引用System.Windows.Forms以便可以在测试中使用MessageBox等。

3、最终C#编写的dll的源代码如下图所示,命名空间为CSharpDll,公共类为CSharpClass。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Forms;

namespace CSharpDll
{
    public class CSharpClass
    {
        public CSharpClass() { }
        public int add(int a , int b)
        {
            return a + b;
        }

        public void substract( int a , int b , ref int c)
        {
            c = a - b;
        }

        public static void showBox(string str)
        {
            MessageBox.Show("C#:" + str);
        }
    }
}

里面包含一个加法add,一个减法substract(为了测试指针,所以在减法的返回类型是void,而把计算结果通过ref参数c给返回),一个showBox方法(里面采用C#的MessageBox对话框显示用户输入的参数字串)

4、对project进行release build,在release目录下生成了CSharpDll.dll(待会用到)。

5、关闭CSharpDll项目,另外新建一个C++ CLR类型的Class Library项目(选择与C#项目相同的.Net Framework 4),项目名称为CppDll。

一开始我用的VS2019,发现VS2019好像无法新建 C++ CLR类型的Class Library项目了,所以学习微软的技术一定要小心,学习主流的支持很久的技术,尽量不要学习新出的技术,如果必须学新技术,一定要认真考量,一些边缘化的技术一定不要学习,没准哪天微软就不维护了。

6、选择Project->CppDll Properties…,在弹出的属性页面选择“Add New Reference..”,点击“browsing.”后选择CSharpDll项目中release目录下的CSharpDll.dll。

7、选择CSharpDll.dll后,可以看到在项目属性的References中出现了CSharpDll这个Library。

8、在CppDll项目中的CppDll.h中利用_declspec(dllexport)导出对应的3个接口函数add,substract,showBox。需要using namespace System::Reflection,对于这里的showBox,其参数不能采用CSharpDll里面的showBox参数的String类型,而是使用const char* 类型。

主要代码如下所示:

// CppDll.h

#pragma once

using namespace System;
using namespace System::Reflection;

__declspec(dllexport) int add(int a, int b)
{
	CSharpDll::CSharpClass obj;
	return obj.add(a, b);
}

__declspec(dllexport) void substract(int a, int b, int *c)
{
	CSharpDll::CSharpClass obj;
	obj.substract(a, b, *c);
}

__declspec(dllexport) void showBox(const char* content)
{
	CSharpDll::CSharpClass obj;
	String^ str = gcnew String(content);
	obj.showBox(str);
}

namespace CppDll {

	public ref class Class1
	{
		// TODO:  在此处添加此类的方法。
	};
}

9、选择release方式build CppDll项目,在release文件夹中生成了CppDll.dll文件,可以看到同时其也将引用的CSharpDll.dll也给拷贝到release文件夹中了。

10、接下来在Qt中进行调用, 在QtCreator中新建一个TestCSharpDll GUI项目,编译器选的mingw。通过VS自带的命令行工具中的dumpbin工具可以查看CppDll.dll导出的函数接口。

dumpbin -exports (+dll路径)

在TestCSharpDll工程中通过typedef定义函数指针,同时采用QLibrary动态加载并resolve函数。

在这里.dll的路径设为当前目录下“./CppDllMingW.dll”,也就是编译好的程序exe同一目录下的dll,去resolve由普通导出方式的接口即“?add@@YAHHH@Z”。

主要代码如下所示:

#include "mainwindow.h"
#include "ui_mainwindow.h"
#include<QLibrary>
#include<QMessageBox>
typedef int (*x_add)(int a , int b);
typedef void (*x_substract)(int a , int b , int* c);
typedef void (*x_showBox)(const char* content);

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);
}

MainWindow::~MainWindow()
{
    delete ui;
}

//add
void MainWindow::on_pushButton_clicked()
{
    int a = ui->lineEdit->text().toInt();
    int b = ui->lineEdit_2->text().toInt();
    QLibrary library("./CppDll.dll");
    if(library.load()){
        x_add add = (x_add)library.resolve("?add@@YAHHH@Z");
        if(add){
            QString str = QString::number(add(a , b));
            QMessageBox::information(this , "call add from dll" , str);
        }
    }
}

//sub
void MainWindow::on_pushButton_2_clicked()
{
    int a = ui->lineEdit_3->text().toInt();
    int b = ui->lineEdit_4->text().toInt();
    int c = 0;
    QLibrary library("./CppDll.dll");
    if(library.load()){
        x_substract sub = (x_substract)library.resolve("?substract@@YAXHHPAH@Z");
        if(sub){
            sub(a , b , &c);
            QString str = QString::number(c);
            QMessageBox::information(this , "call sub from dll" , str);
        }
    }
}

//showBox
void MainWindow::on_pushButton_3_clicked()
{
    QLibrary library("./CppDll.dll");
    if(library.load()){
        x_showBox showBox = (x_showBox)library.resolve("?showBox@@YAXPBD@Z");
        if(showBox){
            showBox("showBox!");
        }
    }
}

编译TestCSharpDll工程,将CppDll.dll和CSharpDll.dll复制到同一目录下,执行TestCSharpDll.exe,可看出点击按钮后,通过QLibrary进行动态resolve,均正常调用。

调用add函数
调用sub函数
调用showBox函数

最好是将相关dll置于同一目录下运行,不然会出现“未能加载文件或程序集”的异常。针对.lib链接方式,理应是置于同一目录下。而针对QLibrary进行resolve方式,可能通常一开始的想法是,CppDll.dll和CSharpDll.dll放在与程序不同目录的地方,程序中利用了QLibrary指定了CppDll.dll的方式进行加载,而CppDll.dll和CSharpDll.dll,因此程序调用CppDll.dll里面的函数时,CppDll.dll会找到与CppDll.dll同一目录下的CSharpDll.dll,然而CppDll.dll在被程序进行加载时,其继承了程序的环境变量,因此会从程序的当前目录下去查找,所以最好还是将CppDll.dll和CSharpDll.dll放置于程序同一目录下,同时QLibrary加载当前目录下的CppDll.dll。当然,部署到另外一台机器上时,目标机器还是需要安装.Net Framework,其版本至少不能低于当前CSharpDll.dll所使用的版本。

C++的进入点是main()吗?

什么代码比main()更早被执行?

在linux下我们可以写一个startup code,通常情况下main()是通过startup code调用的,所以startup code会比main()更早执行,它是C++真正的进入点(entry point)。

下面我们来写一段startup code:

首先创建一个entrypoint.c文件(.cpp也可以),写入我们的startup code。

在这个startup中我们有两个函数,一个blabla()函数,一个main()函数,接下来我们将blabla()函数设置为entry pont(C/C++程序进入点),使用gcc的-e指令进行编译,就可以以告诉链接器使用blabla作为程序进入点,执行结果如下:

我们可以看到,main()函数并没有被startup code调用,所以并没有被执行。

而startup code实际上为一个函数,所以startup code一定是C/C++程序中最早被执行的函数。

而在Windows下的C++程序又分为命令行(console),窗体程序(Win32),以及动态链接库(WinDll),他们的主函数名称各不相同,startup code会争对不同的C++程序调用不同的主函数。

在VC6.0下,默认的Startup code由4个文件组成,分别为crt0.c,wcrt0.c,wincrt0.c,wwincrt0.c,而wcrt0.c,wincrt0.c,wwincrt0.c这三个文件全部include了crt0.c,所以startup code的核心全部在crt0.c中。

一、配置python环境问题

1.首先安装Python(版本无所谓),安装的时候选的添加python路径到环境变量中

安装之后的文件夹如下所示:

2.在VS中配置环境和库

右击项目->属性->VC++目录

1)包含目录:

Python安装路径/include

2)库目录:

Python安装路径/libs

右击项目->属性->连接器->输入->附加依赖库

debug下:
python安装目录/libs/python37_d.lib
release下:
python安装目录/libs/python37.lib

注意
1、debug配置的时候可能没有python37_d.lib,那就把python37.lib复制一个,然后重命名为python37_d.lib就可以啦
2、如果一直报错,但是包含头文件等都没有问题,那么你需要看一下你的python是32位还是64位的。然后根据python的环境去配置vs的环境。

二、C++调用python函数并输出返回值

首先可能有个坑!一定要保证这个python函数所在的文件能够正常运行!然后把python代码放到和C++代码同一目录下。

1、定义Python函数

#!python3
# -*- coding:utf-8 -*-
import base64
import  hmac
from hashlib import sha1

def hash_hmac(code , key):
    #sha1加密签名算法
    hmac_code = hmac.new(key.encode() , code.encode() , sha1).digest()
    return base64.b64encode(hmac_code).decode()

2、编写C++代码

#include <iostream>
#include<python.h>
using namespace std;
int main()
{

    Py_Initialize();//使用python之前,要调用Py_Initialize();这个函数进行初始化
    if (!Py_IsInitialized())
    {
        printf("初始化失败!");
        return 0;
    }

    PyRun_SimpleString("import sys");
    PyRun_SimpleString("sys.path.append('./')");//这一步很重要,修改Python路径


    PyObject* pModule = NULL;//声明变量
    PyObject* pFunc = NULL;// 声明变量

    pModule = PyImport_ImportModule("hash_hmac");//这里是要调用的文件名hash_hmac.py
    if (pModule == NULL)
    {
        cout << "没找到" << endl;
    }

    pFunc = PyObject_GetAttrString(pModule, "hash_hmac");//这里是要调用的函数名
    //两个字符串参数
    PyObject* pParams = Py_BuildValue("(ss)", "/oss/upload?bucket=test&filekey=test/image/3b/3ba9d94cab2f8868823d71c4445e125a.png\n" , "q4mJAS777BUbbdVpEqh2XRcZZqNyDweU4GRnM690");
    char* result;
    PyObject* pRet = PyObject_CallObject(pFunc, pParams);//调用函数

    int res = 0;
    PyArg_Parse(pRet, "s", &result);//转换返回类型

    cout << "res:" << result << endl;//输出结果

    Py_Finalize();//调用Py_Finalize,这个根Py_Initialize相对应的。

    return 0;
}

3、解释部分C++代码

PyObject* pParams = Py_BuildValue("(ss)", "/oss/upload?bucket=test&filekey=test/image/3b/3ba9d94cab2f8868823d71c4445e125a.png\n" , "q4mJAS777BUbbdVpEqh2XRcZZqNyDweU4GRnM690");

在这里我输入了两个字符串类型的参数,Py_BuildValue()函数的作用和PyArg_ParseTuple()的作用相反,它将C类型的数据结构转换成Python对象。

该函数可以和PyArg_ParseTuple()函数一样识别一系列的格式串,但是输入参数只能是值,而不能是指针。

它返回一个Python对象和PyArg_ParseTuple()不同的一点是PyArg_ParseTuple()函数它的第一个参数为元组,Py_BuildValue()则不一定会生成一个元组。它生成一个元组仅仅当格式串包含两个或者多个格式单元,如果格式串为空,返回NONE。
在下面的描述中,括号中的项是格式单元返回的Python对象类型,方括号中的项为传递的C的值的类型。
“s” (string) [char *] :将C字符串转换成Python对象,如果C字符串为空,返回NONE。
“s#” (string) [char *, int] :将C字符串和它的长度转换成Python对象,如果C字符串为空指针,长度忽略,返回NONE。
“z” (string or None) [char *] :作用同”s”。
“z#” (string or None) [char *, int] :作用同”s#”。
“i” (integer) [int] :将一个C类型的int转换成Python int对象。
“b” (integer) [char] :作用同”i”。
“h” (integer) [short int] :作用同”i”。
“l” (integer) [long int] :将C类型的long转换成Pyhon中的int对象。
“c” (string of length 1) [char] :将C类型的char转换成长度为1的Python字符串对象。
“d” (float) [double] :将C类型的double转换成python中的浮点型对象。
“f” (float) [float] :作用同”d”。
“O&” (object) [converter, anything] :将任何数据类型通过转换函数转换成Python对象,这些数据作为转换函数的参数被调用并且返回一个新的Python对象,如果发生错误返回NULL。
“(items)” (tuple) [matching-items] :将一系列的C值转换成Python元组。
“[items]” (list) [matching-items] :将一系列的C值转换成Python列表。
“{items}” (dictionary) [matching-items] :将一系类的C值转换成Python的字典,每一对连续的C值将转换成一个键值对。

例如:
Py_BuildValue(“”) None
Py_BuildValue(“i”, 123) 123
Py_BuildValue(“iii”, 123, 456, 789) (123, 456, 789)
Py_BuildValue(“s”, “hello”) ‘hello’
Py_BuildValue(“ss”, “hello”, “world”) (‘hello’, ‘world’)
Py_BuildValue(“s#”, “hello”, 4) ‘hell’
Py_BuildValue(“()”) ()
Py_BuildValue(“(i)”, 123) (123,)
Py_BuildValue(“(ii)”, 123, 456) (123, 456)
Py_BuildValue(“(i,i)”, 123, 456) (123, 456)
Py_BuildValue(“[i,i]”, 123, 456) [123, 456] Py_BuildValue(“{s:i,s:i}”, “abc”, 123, “def”, 456) {‘abc’: 123, ‘def’: 456}
Py_BuildValue(“((ii)(ii)) (ii)”, 1, 2, 3, 4, 5, 6) (((1, 2), (3, 4)), (5, 6))

3、运行C++程序

与Python代码的预期相同。

三、Python代码处理

在发布软件的时候,通常我们都不希望代码可以直接被别人看到。

以上的Debug目录中的exe要想能够单独运行,必须把python脚本拷过去。为了不让别人能直接看到我的代码,我拷过去的是生成的.pyc文件,实现了一个简单的python代码的加密。不过据说可以反编译,但是对我来说已经够了。

四、.py和.pyc的区别

原来Python的程序中,是把原始程序代码放在.py文件里,而Python会在执行.py文件的时候。将.py形式的程序编译成中间式文件(byte-compiled)的.pyc文件,这么做的目的就是为了加快下次执行文件的速度。

所以,在我们运行python文件的时候,就会自动首先查看是否具有.pyc文件,如果有的话,而且.py文件的修改时间和.pyc的修改时间一样,就会读取.pyc文件,否则,Python就会读原来的.py文件。

其实并不是所有的.py文件在与运行的时候都会产生.pyc文件,只有在import相应的.py文件的时候,才会生成相应的.pyc文件。

五、使用.py生成.pyc

在命令行下使用下列命令即可:

python -m py_compile test.py#单文件

python -m py_compile /root/src/{file1,file2}.py#多文件