Jack Huang's Blog


  • 首页

  • 标签

  • 归档

  • 搜索

C++跨平台移植开发思考

发表于 2018-07-16 | 更新于 2022-07-17

最近需要将一份C++代码通过Emscripten编译成asm.js,结果失败,因为这份C++代码使用MFC GUI库,导致Emscripten编译失败。这引起了我对C++跨平台移植的思考。

C++代码理论上能实现源代码级别的跨平台移植,即同一份代码可以不加修改的在不同的平台上编译运行且表现一致。这要求编写的C++代码符合跨平台移植的规范,如链接[1]所指出的那样,它给出了很多实现C++代码跨平台移植的编码细节,在编写跨平台的底层库方面十分有用。但在编写跨平台的C++应用方面,应使用经过长期实践证明能用的跨平台库来帮助编码,建立在巨人的肩膀上,使跨平台C++代码编写工作变得简单容易。下面给出一些常用的跨平台C++库供参考:

  1. 语言及基础库
  • 标准 C++:标准 c++ 是98年制定的,现在主流的 c++ 编译器都能够比较好的支持了。这里建议使用VC7.1和GCC4.0及以上版本。在Linux中,glibc是标准C的实现,libstdc++则是标准C++的实现。在Windows中,VS2015之前MSVCRT.DLL是标准C/C++的实现,之后UCRTBASE.DLLz则是标准C/C++的实现。
  • boost:boost 则是 C++ 标准委员会的一群人弄起来的一个 C++ 库集合,其中不少库以经进入 C++ TR1,可以说是准标准。使用这里的库我们有着充分的理由。象字符串的操作可以用 boost 的 String algorithms 库,格式化操作可以用 boost::format,正则式可用 boost::regex 等等。
  1. 网络
  • ACE(Adaptive Communication Environment):ACE是一个以C++的Template技术所做成的开放源代码的可跨平台的网络应用程序的程式库套件。ACE自适配通信环境(ADAPTIVE Communication Environment)是可以自由使用、开放源码的面向对象(OO)框架(Framework),在其中实现了许多用于并发通信软件的核心模式。ACE提供了一组丰富的可复用C++ Wrapper Facade(包装外观)和框架组件,可跨越多种平台完成通用的通信软件任务,其中包括:事件多路分离和事件处理器分派、信号处理、服务初始化、进程间通信、共享内存管理、消息路由、分布式服务动态(重)配置、并发执行和同步,等等。
  • Boost.Asio:用于网络和底层I/O编程的跨平台的C++库
  1. 数据库
  • OTL (Oracle, Odbc and DB2-CLI Template Library):
    不但支持跨平台,还跨数据库。OTL支持以下面的数据库 : OTL Oracle ,SQL Server, Access, MySQL。还有其它的库如DTL,这个库不但支持ODBC,它还支持数据库的原生接口,可以有更佳的性能。
  1. GUI
  • QT:QT是双认证的,当你的程序免费时它就免费,你的程序打算卖钱时,它也要,而且要价很高。所以,如果有版权,成本上的考虑的话,则可以考虑wxWidgets。
  • wxWidgets:wxWidgets(/wɪksˈwɪdʒɪts/,原名wxWindows)是一个开放源代码且跨平台的对象工具集(widget toolkit),其库可用来创建基本的图形用户界面(GUI)。wxWidgets由Julian Smart于1992年首先开发。
  1. 科学计算
  • GSL:GNU科学库。
  1. 游戏开发
  • Cocos2d-x:一个跨平台框架,用于构建2D游戏,互动图书,演示和其他图形应用程序。
  1. 视频
  • FFmpeg:一个完整的,跨平台的解决方案,用于记录,转换视频和音频流

参考文献

  1. c++跨平台移植指南, by 洪柏敏.
  2. ACE自适配通信环境,by wikipedia.
  3. C++库汇总, by 工程师WWW.
  4. 值得推荐的C/C++框架和库, by zhihu.
  5. printf 格式化输出符号详细说明,by jackytse_.
  6. 理一理字节对齐的那些事,by 守望.
  7. 不见得你会计算C字符串长度,by veryitman.
  8. 浅析C语言之uint8_t / uint16_t / uint32_t /uint64_t,by 海阔天空sky1992.
  9. char,int,float,double所占字节数,by 张小铭.
  10. c++ 时间类型详解 time_t,by runoob.
  11. 网络传输——序列化,by bw_0927.
  12. 干货:构建C/C++良好的工程结构,by Froser.
  13. UTF-8与UTF-8 without BOM,by 苏州-微尘.

Emscripten教程

发表于 2018-07-15 | 更新于 2018-09-09

Emscripten是一种基于LLVM的编译器,理论上能够将任何能够生成LLVM位码的代码编译成javascript的严格子集asm.js,实际上主要用于将C/C++代码编译成asm.js。本文主要介绍Emscripten的安装过程。

下载和安装

从源码编译安装十分麻烦,推荐安装核心的Emscripten SDK。以Windows为例,先使用如下命令下载emsdk。

1
2
3
4
5
# Get the emsdk repo
git clone https://github.com/juj/emsdk.git

# Enter that directory
cd emsdk

再使用如下命令安装配置Emscripten。

1
2
3
4
5
6
7
8
9
10
11
# Fetch the latest registry of available tools.
.\emsdk.bat update

# Download and install the latest SDK tools. Need install Python first.
.\emsdk.bat install latest

# Make the "latest" SDK "active" for the current user. (writes ~/.emscripten file)
.\emsdk.bat activate latest

# Activate PATH and other environment variables in the current terminal
.\emsdk_env.bat

验证

使用如下命令验证Emscripten是否安装配置正确。

1
2
3
4
5
6
7
8
# Enter that directory
cd emsdk

# Activate PATH and other environment variables in the current terminal
.\emsdk_env.bat

# Verifying Emscripten
emcc.bat -v

运行

如果验证通过,即可使用Emscripten编译C/C++代码到asm.js。

创建名为helloWorld.cpp的文件,其内容如下:

1
2
3
4
5
6
#include <stdio.h>

int main() {
printf("hello, world!\n");
return 0;
}

使用如下命令编译:

1
emcc.bat helloWorld.cpp

编译后将生成a.out.js和a.out.wasm两个文件。后者是包含编译后代码的WebAssembly文件,前者是用于加载和执行后者的Javascipt文件。使用如下命令测试编译后生成的代码,将输出“hello,world!”。

1
node a.out.js

参考链接

  1. http://kripken.github.io/emscripten-site/docs/introducing_emscripten/about_emscripten.html. by kripken.

浏览器中运行3D游戏的思考

发表于 2018-07-10 | 更新于 2018-11-20

Web技术突飞猛进,几乎无所不能,无所不在。然而在3D游戏领域,web技术乏善可陈,性能瓶颈问题制约其发展。但是开发者们没有放弃这个梦想,不断努力,孜孜不倦地改进和增强浏览器中运行3D游戏性能的方法和技术,目前已能看到一丝曙光。

对C/S架构的3D游戏而言,渲染和计算通常都在客户端,服务器端负责用户状态的管理和分发。同样对B/S架构的运行在浏览器中的3D游戏,所有的渲染和计算也应该在浏览器端。如果渲染和计算放在服务器端,将导致可怕的延迟,并严重损害3D游戏的可伸缩性。

当前很多3D游戏使用C/C++语言编写,如果能够将C/C++语言编译成JavaScript语言,可大大促进Web 3D游戏的开发。而编译器项目Emscripten正是一个这样的工具。它能将C/C++代码编译成一种叫做asm.js的Javascript变体。需要指出的是,Emscripten的输入,即C/C++代码最好是开放源代码的,因为Emscripten不支持闭源代码的编译。例如Emscripten不支持mfc程序的转换,因为mfc是闭源的[4]。

asm.js是一个JavaScript的严格子集,它只提供32位带符号整数和64位带符号浮点数两种数据类型,其他Javascript类型如字符串、布尔值等以数值形式存在,保存在内存中,通过TypedArray调用。另外asm.js没有垃圾回收机制,所有内存操作都由程序员自己控制。asm.js是优化后的JavaScript,它在浏览器中的运行速度大约是原生代码的一倍左右[1]。

asm.js虽然比原生javascript运行速度快了一倍左右,但是相比C/C++代码,运行速度还是有差距。幸运的是出现了名为WebAssembly的技术。WebAssembly或称wasm是一个实验性的低级编程语言,应用于浏览器内的客户端[5]。WebAssembly是便携式的抽象语法树,被设计来提供比JavaScript更快速的编译及运行。WebAssembly将让开发者能运用自己熟悉的编程语言(最初以C/C++作为实现目标)编译,再藉虚拟机引擎在浏览器内运行。WebAssembly的开发团队分别来自Mozilla、Google、Microsoft、Apple,代表着四大网络浏览器Firefox、Chrome、Microsoft Edge、Safari。2017年11月,所有以上四个浏览器都开始实验性的支持WebAssembly。目前,最新版本的Emscripten已支持将C/C++代码编译成wasm。

参考文献

  1. http://www.ruanyifeng.com/blog/2017/09/asmjs_emscripten.html. by 阮一峰.
  2. https://www.cnblogs.com/slly/p/6639173.html. by 李某龙.
  3. http://kripken.github.io/emscripten-site/. by emscripten.
  4. https://github.com/kripken/emscripten/issues/941. by emscripten.
  5. https://zh.wikipedia.org/wiki/WebAssembly. by wikipedia.

Cookie,Session和Token会话知识整理

发表于 2018-07-08 | 更新于 2022-07-22

HTTP是一种无状态的协议,然而当服务器端需要判断用户能否访问某些资源,记录用户的购物车内容等场景时,就需要一种机制维护会话状态,这时候Cookie、Session和Token就派上了用场。

Cookie

Cookie技术最早用于解决HTTP的会话问题, 它是 http 协议的一部分,它的处理分为如下几步:

  • 服务器向客户端发送 cookie。
    • 通常使用 HTTP 协议规定的 set-cookie 头操作。
    • 规范规定 cookie 的格式为 name = value 格式,且必须包含这部分。
  • 浏览器将 cookie 保存。
  • 每次请求浏览器都会将 cookie 发向服务器。

可选的Cookie参数

其他可选的 cookie 参数会影响将 cookie 发送给服务器端的过程,主要有以下几种:

  • path:表示 cookie 影响到的路径,匹配该路径才发送这个 cookie。
  • expires 和 maxAge:告诉浏览器这个 cookie 什么时候过期,expires 是 UTC 格式时间,maxAge 是 cookie 多久后过期的相对时间。当不设置这两个选项时,会产生 session cookie,session cookie 是 transient 的,当用户关闭浏览器时,就被清除。一般用来保存 session 的 session_id。
  • secure:当 secure 值为 true 时,cookie 在 HTTP 中是无效,在 HTTPS 中才有效。
  • httpOnly:浏览器不允许脚本操作 document.cookie 去更改 cookie。一般情况下都应该设置这个为 true,这样可以避免被 xss 攻击拿到 cookie。

常用场景

当给Cookie设置expires和maxAge后,在未到期前,浏览器端的Cookie不会因为浏览器的关闭而消失。该特性常用于自动登录,记录用户浏览信息。例如很多购物网站常用该特性记录用户的喜好和购买的物品。

Cookie安全隐患

Cookie提供了一种手段使得HTTP请求可以附加当前状态, 大多数网站就是靠Cookie来标识用户的登录状态的,例如:

  1. 用户提交用户名和密码的表单,这通常是一个POST HTTP请求。
  2. 服务器验证用户名与密码,如果合法则返回200(OK)并设置Set-Cookie为authed=true。
  3. 浏览器存储该Cookie。
  4. 浏览器发送请求时,设置Cookie字段为authed=true。
  5. 服务器收到第二次请求,从Cookie字段得知该用户已经登录。 按照已登录用户的权限来处理此次请求。

上述认证流程存在安全隐患,因为Cookie是可以被篡改的。如果使用一些HTTP客户端软件,设置Cookie字段为authed=true并发送该HTTP请求,服务器就会被欺骗。

Cookie防篡改机制

服务器为每个Cookie项生成签名,可有效地防止Cookie被篡改。因为用户篡改Cookie后无法生成对应的签名, 服务器便可得知用户对Cookie进行了篡改。一个简单的校验过程可能是这样的:

  1. 在服务器中配置一个不为人知的字符串(我们叫它Secret),比如:x$sfz32。
  2. 当服务器需要设置Cookie时(比如authed=false),不仅设置authed的值为false, 在值的后面进一步设置一个签名,最终设置的Cookie是authed=false|6hTiBl7lVpd1P。
  3. 签名6hTiBl7lVpd1P是这样生成的:Hash(‘x$sfz32’+’false’)。 要设置的值与Secret相加再取哈希。
  4. 用户收到HTTP响应并发现头字段Set-Cookie: authed=false|6hTiBl7lVpd1P。
  5. 用户在发送HTTP请求时,篡改了authed值,设置头字段Cookie: authed=true|???。 因为用户不知道Secret,无法生成签名,只能随便填一个。
  6. 服务器收到HTTP请求,发现Cookie: authed=true|???。服务器开始进行校验: Hash(‘true’+’x$sfz32’),便会发现用户提供的签名不正确。

通过给Cookie添加签名,使得服务器得以知道Cookie被篡改。然而故事并未结束。

因为Cookie是明文传输的, 只要服务器设置过一次authed=true|xxxx我不就知道true的签名是xxxx了么, 以后就可以用这个签名来欺骗服务器了。因此Cookie中最好不要放敏感数据。 一般来讲Cookie中只会放一个Session Id,而Session存储在服务器端。

Session

为了解决Cookie的安全隐患,Session机制应运而生。session机制是一种服务器端的机制,它存储在服务器端的,避免了在客户端Cookie中存储敏感数据。Session可以存储在HTTP服务器的内存中,也可以存在内存数据库(如redis)中, 对于重量级的应用甚至可以存储在数据库中。

客户端对服务端请求时,服务端会检查请求中是否包含一个session标识( 称为session id ).

  • 如果没有,那么服务端就生成一个随机的session以及和它匹配的session id,并将session id返回给客户端.
  • 如果有,那么服务器就在存储中根据session id 查找到对应的session.

基于Session的登录流程

一个简单的使用Session机制的登录流程可能是这样的:

  1. 用户提交包含用户名和密码的表单,发送HTTP请求。
  2. 服务器验证用户发来的用户名密码。
  3. 如果正确则把当前用户名(通常是用户对象)存储到redis中,并生成它在redis中的ID。这个ID称为Session ID,通过Session ID可以从Redis中取出对应的用户对象, 敏感数据(比如authed=true)都存储在这个用户对象中。
  4. 设置Cookie为sessionId=xxxxxx|checksum并发送HTTP响应, 仍然为每一项Cookie都设置签名。
  5. 用户收到HTTP响应后,便看不到任何敏感数据了。在此后的请求中发送该Cookie给服务器。
  6. 服务器收到此后的HTTP请求后,发现Cookie中有SessionID,进行放篡改验证。
  7. 如果通过了验证,根据该ID从Redis中取出对应的用户对象, 查看该对象的状态并继续执行业务逻辑。

Session安全隐患

Session ID作为Cookie存储在浏览器端,因此存在被劫持的风险,尤其是开发者没有正确的关闭会话。用户关闭会话时,应删除传递 Session ID 的 Cookie,同时撤销服务器端的Session内容。例如:

1
2
3
4
5
6
7
8
9
10
11
12
/* 普通用户登出 */
router.post('/signout', function(req, res, next) {
if (_.isEmpty(req.body) === false) {
req.session.account = null; // 删除session
res.json({
message: '登出成功!'
});

} else {
res.send(406, { message: 'The params is not correct!' });
}
});

Token

Token是用户的验证方式,最简单的token组成:uid(用户唯一的身份标识)、time(当前时间的时间戳)、sign(签名,由token的前几位+盐以哈希算法压缩成一定长的十六进制字符串,可以防止恶意第三方拼接token请求服务器)。

基于Token的身份验证流程

使用基于 Token 的身份验证方法,在服务端不需要存储用户的登录记录。大概的流程是这样的:

  1. 客户端使用用户名跟密码请求登录
  2. 服务端收到请求,去验证用户名与密码
  3. 验证成功后,服务端会签发一个 Token,再把这个 Token 发送给客户端
  4. 客户端收到 Token 以后可以把它存储起来,比如放在 Cookie 里或者 Local Storage 里
  5. 客户端每次向服务端请求资源的时候需要带着服务端签发的 Token
  6. 服务端收到请求,然后去验证客户端请求里面带着的 Token,如果验证成功,就向客户端返回请求的数据

基于Token方法的优势

  • JWT 方法允许我们进行AJAX调用任何服务器或域。由于HTTP头是用来传输用户信息的。
  • 没必要在服务器存储一个单独的session。JWT本身传达全部的信息。
  • 服务器端减少到只是一个API和可以通过CDN服务的静态资源(HTML,CSS,JS)。
  • 认证系统是手机兼容的,任何设备上可以生成令牌。
  • 由于已经消除了cookie的需要,也不再需要保护跨站请求。
  • API密钥提供非此即彼的解决方案,然而JWT提供更颗粒度的控制,它可以用于任何调试目的的检查。
  • API密钥依赖于中央存储和服务。JWT可以自发行或者外部服务在允许的范围和期限发布它。

JWT结构

Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。

一个JWT实际上就是一个字符串,它由三部分组成,头部、载荷与签名。

Cookie、Session和Token对比

cookie与session的区别

  1. cookie数据存放在客户端上,session数据放在服务器上。
  2. cookie不是很安全,别人可以分析存放在本地的COOKIE并进行COOKIE欺骗。考虑到安全应当使用session。
  3. session会在一定时间内保存在服务器上。当访问增多,会比较占用你服务器的性能。考虑到减轻服务器性能方面,应当使用COOKIE。

session与token的区别

作为身份认证token安全性比session好,因为每个请求都有签名还能防止监听以及重放攻击。

Session 是一种HTTP存储机制,目的是为无状态的HTTP提供的持久机制。Session 认证只是简单的把User 信息存储到Session 里,因为SID 的不可预测性,暂且认为是安全的。这是一种认证手段。 但是如果有了某个User的SID,就相当于拥有该User的全部权利.SID不应该共享给其他网站或第三方。

Token, 如果指的是OAuth Token 或类似的机制的话,提供的是 认证 和 授权,认证是针对用户,授权是针对App。其目的是让某App有权利访问某用户的信息。这里的Token是唯一的。不可以转移到其它App上,也不可以转到其它用户上。

参考链接

  1. cookie 和 session, by 极客学院.
  2. Cookie/Session的机制与安全, by Harttle Land.
  3. Python中关于JSON网络令牌的实例教程, by Python部落.
  4. 什么是 JWT – JSON WEB TOKEN, by Dearmadman
  5. JSON Web Token 入门教程,by 阮一峰.
  6. localForage,by github.
  7. node session 实现登录状态持久化,by 开心的米卡.

兼容前后端多种JS模块规范的代码

发表于 2018-07-02 | 更新于 2019-09-25

最近研究如何在前后端调用同一份js代码。该问题本质是如何兼容前后端各类JS模块规范。上网一搜,已经有很多详细的解决方案和JS模块规范介绍,在这里简单记录一下。

JS模块规范

JS模块规范前端主要有AMD、CMD,后端主要有CommonJS。此外还有最新的ES2015模块规范。

AMD

AMD(异步模块定义,Asynchronous Module Definition)格式总体的目标是为现在的开发者提供一个可用的模块化 JavaScript 的解决方案。它诞生于 Dojo 在使用 XHR+eval 时的实践经验,其支持者希望未来的解决方案都可以免受由于过去方案的缺陷所带来的麻烦。

AMD风格模块定义通常包括:一个用来进行模块定义的 define 方法以及一个用来处理依赖项加载的 require 方法。define 根据如下的方法签名来定义具名或匿名的模块:

1
2
3
4
5
define(
module_id /*可选*/,
[dependencies] /*可选*/,
definition function /*用来初始化模块或对象的函数*/
);

CMD

CMD(Common Module Definition)表示通用模块定义,该规范是国内发展出来的,由阿里的玉伯提出。就像AMD有个requireJS,CMD有个浏览器的实现SeaJS,SeaJS和requireJS一样,都是javascript的前端模块化解决方案。
CMD规范简单到只有一个API,即define函数:

1
2
3
4
5
define(function(require, exports, module) {

// The module code goes here

});

CMD 与 AMD 挺相近,二者区别如下:

  • 对于依赖的模块 CMD 是延迟执行,而 AMD 是提前执行(不过 RequireJS 从 2.0 开始,也改成可以延迟执行。 )
  • CMD 推崇依赖就近,AMD 推崇依赖前置。
  • AMD 的 api 默认是一个当多个用,CMD 严格的区分推崇职责单一,其每个 API 都简单纯粹。例如:AMD 里 require 分全局的和局部的。CMD 里面没有全局的 require,提供 seajs.use() 来实现模块系统的加载启动。

CommonJS

CommonJS是一个志愿性质的工作组,它致力于设计、规划并标准化 JavaScript API。至今为止他们已经尝试着认可了模块标准以及程序包标准。CommonJS 的模块提案为在服务器端声明模块指定了一个简单的 API。不像 AMD,它试图覆盖更宽泛的方面比如 IO、文件系统、promise 模式等等。CommonJS风格模块在nodejs中得到广泛的应用。

CommonJS风格模块是一段可重用的 JavaScript,它导出一系列特定的对象给依赖它的代码调用——通常来说这样的模块外围没有函数包裹(所以你在这里的例子中不会看到 define)。例如:

1
2
3
4
5
6
7
8
9
10
// package/lib 是我们须要的一个依赖项
var lib = require('package/lib');

// 我们的模块的一些行为
function foo(){
lib.log('hello world!');
}

// 把 foo 导出(暴露)给其它模块
exports.foo = foo;

ES2015模块规范

2015 年 6 月, ES2015(即 ECMAScript 6、ES6) 正式发布。ES2015 是该语言的一个显著更新,也是自 2009 年 ES5 标准确定后的第一个重大更新。

ES2015的模块规范如下:

  • 一个模块就是一个独立的文件。该文件内部的所有变量,外部无法获取。
  • export 命令用于规定模块的对外接口。
  • import 命令用于输入其他模块提供的功能。
  • ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。

ES2015模块示例如下:

1
2
3
4
5
6
7
8
9
//circle.js
//圆面积计算
export function area(radius) {
return Math.PI * radius * radius;
}

//main.js
import {area} from './hangge';
console.log('圆面积:' + area(10));

兼容多种模块规范

需要指出的是下面兼容多种JS模块规范的代码并不支持最新的ES2015模块规范。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
;(function (name, definition) {
// 检测上下文环境是否为AMD或CMD
var hasDefine = typeof define === 'function',
// 检查上下文环境是否为Node
hasExports = typeof module !== 'undefined' && module.exports;

if (hasDefine) {
// AMD环境或CMD环境
define(definition);
} else if (hasExports) {
// 定义为普通Node模块
module.exports = definition();
} else {
// 将模块的执行结果挂在window变量中,在浏览器中this指向window对象
this[name] = definition();
}
})('hello', function () {
var hello = function () {};
return hello;
});

Webpack兼容所有JS模块规范

webpack根据webpack.config.js中的入口文件,在入口文件里识别模块依赖,不管这里的模块依赖是用CommonJS写的,还是ES6 Module规范写的,webpack会自动进行分析,并通过转换、编译代码,打包成最终的文件。最终文件中的模块实现是基于webpack自己实现的webpack_require(es5代码),所以打包后的文件可以跑在浏览器上。

同时以上意味着在webapck环境下,你可以只使用ES6 模块语法书写代码(通常我们都是这么做的),也可以使用CommonJS模块语法,甚至可以两者混合使用。因为从webpack2开始,内置了对ES6、CommonJS、AMD 模块化语句的支持,webpack会对各种模块进行语法分析,并做转换编译。

具体Webpack兼容所有JS模块规范的原理分析参见Webpack 模块打包原理。

参考链接

  1. 兼容前后端共用模块代码(摘自《深入浅出Node.js》), by Jake.
  2. 兼容多种模块规范(AMD,CMD,Node)的代码, by CodeMan.
  3. 使用 AMD、CommonJS 及 ES Harmony 编写模块化的 JavaScript, by ADDY OSMANI.
  4. JS - CommonJS、ES2015、AMD、CMD模块规范对比与介绍(附样例),by hangge.
  5. What is Babel?,by Babel.
  6. Webpack Concepts,by webpack.
  7. import、require、export、module.exports 混合使用详解,by lv_DaDa.
  8. Webpack 模块打包原理,by lq782655835.

sequelize-cli使用经验总结

发表于 2018-06-22 | 更新于 2022-10-17

Sequelize 是一个基于 promise 的 Node.js ORM, 目前支持 Postgres, MySQL, SQLite 和 Microsoft SQL Server. 它具有强大的事务支持, 关联关系, 读取和复制等功能[2]. 下面主要介绍如何使用sequelize-cli工具快速构建nodejs的数据访问层。

基于express创建nodejs web应用

基于express建立nodejs的web应用,在此基础上再使用sequlize建立数据访问层和数据库。

1
2
3
4
mkdir myapp
cd myapp
express
npm install

基于sequelize创建数据访问层和数据库

安装sequlize

1
2
npm install --save sequelize
npm install --save mysql2

安装sequelize-cli

首先应安装sequelize-cli工具,可以选择全局安装,也可以选择本地安装。

1
2
npm install --save-dev sequelize-cli
npm install -g sequelize-cli

建立初始的ORM引导框架

使用如下命令在Nodejs Web应用中建立初始的数据访问层。

1
2
3
mkdir db
cd db
../node_modules/.bin/sequelize init

这将创建以下文件夹:

  • config, 包含配置文件,它告诉CLI如何连接数据库
  • models,包含您的项目的所有模型
  • migrations, 包含所有迁移文件
  • seeders, 包含所有种子文件

修改配置文件以连接到数据库管理系统,并创建数据库

在建立模型之前,应先修改config/config.json,以告诉 CLI 如何连接到数据库。config/config.json内容如下:

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
{
"development": {
"username": "root",
"password": "root",
"database": "database_development",
"host": "127.0.0.1",
"dialect": "mysql",
"pool":{
"max": 5,
"min": 0,
"idle": 10000
},
"timezone": "+08:00",
"define":{
"charset": "utf8",
"dialectOptions":{
"collate": "utf8_general_ci"
}
}
},
"test": {
"username": "root",
"password": "root",
"database": "database_test",
"host": "127.0.0.1",
"dialect": "mysql"
},
"production": {
"username": "root",
"password": "root",
"database": "database_test",
"host": "127.0.0.1",
"dialect": "mysql",
"pool":{
"max": 5,
"min": 0,
"idle": 10000
},
"timezone": "+08:00",
"define":{
"charset": "utf8",
"dialectOptions":{
"collate": "utf8_general_ci"
}
}
}
}

上述配置修改了数据库的字符集,使之能支持中文。当然在创建数据库之前,应配置mysql数据库管理系,使其支持utf8字符集。可执行如下命令查看其支持的字符集,如果其不支持uft字符集,请按照参考链接[3]进行修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
mysql> show variables like '%char%';
+--------------------------+---------------------------------------------------------+
| Variable_name | Value |
+--------------------------+---------------------------------------------------------+
| character_set_client | utf8 |
| character_set_connection | utf8 |
| character_set_database | utf8 |
| character_set_filesystem | binary |
| character_set_results | utf8 |
| character_set_server | utf8 |
| character_set_system | utf8 |
| character_sets_dir | C:\Program Files\MySQL\MySQL Server 5.5\share\charsets\ |
+--------------------------+---------------------------------------------------------+
8 rows in set (0.00 sec)

使用如下命令创建数据库:

1
../node_modules/.bin/sequelize db:create

使用如下命令删除数据库:

1
../node_modules/.bin/sequelize db:drop

创建模型

我们将使用 model:generate 命令。 此命令需要两个选项:

  • name, 模型的名称
  • attributes, 模型的属性列表

创建一个名叫 User 的模型:

1
../node_modules/.bin/sequelize model:generate --name User --attributes firstName:string,lastName:string,email:string

这将发生以下事情:

  • 在 models 文件夹中创建了一个 user 模型文件
  • 在 migrations 文件夹中创建了一个名字像 XXXXXXXXXXXXXX-create-user.js 的迁移文件

注意: _Sequelize 将只使用模型文件,它是表描述。另一边,迁移文件是该模型的更改,或更具体的是说 CLI 所使用的表。 处理迁移,如提交或日志,以进行数据库的某些更改。

再创建一个名为Role的模型,它跟User是一对多的关系:

1
../node_modules/.bin/sequelize model:generate --name Role --attributes roleName:string

定义关系

Role和User是一对多的关系,因此需要修改它们的模型定义。
修改models/role.js如下:

1
2
3
4
5
6
7
8
9
10
11
'use strict';
module.exports = (sequelize, DataTypes) => {
var Role = sequelize.define('Role', {
roleName: DataTypes.STRING
}, {});
Role.associate = function(models) {
// associations can be defined here
Role.hasMany(models.User)
};
return Role;
};

修改models/user.js如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
'use strict';
module.exports = (sequelize, DataTypes) => {
var User = sequelize.define('User', {
firstName: DataTypes.STRING,
lastName: DataTypes.STRING,
email: DataTypes.STRING
}, {});
User.associate = function(models) {
// associations can be defined here
User.belongsTo(models.Role, {
onDelete: "NULL",
foreignKey: {
allowNull: false
}
})
};
return User;
};

修改和运行迁移

Role和User是一对多的关系,因此需要修改User迁移文件的定义。
修改migrates/20180622153152-create-user.js如下:

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
34
35
36
37
38
39
40
41
42
43
'use strict';
module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.createTable('Users', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER
},
roleId: { // name of the key we're adding
type: Sequelize.INTEGER,
allowNull: false,
references: {
model: 'Roles', // name of Target model
key: 'id', // key in Target model that we're referencing
},
onUpdate: 'CASCADE',
onDelete: 'CASCADE',
},
firstName: {
type: Sequelize.STRING
},
lastName: {
type: Sequelize.STRING
},
email: {
type: Sequelize.STRING
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
}
});
},
down: (queryInterface, Sequelize) => {
return queryInterface.dropTable('Users');
}
};

注意:这里主要给User添加了外键字段,外键字段命名有规则:通常为小写的模型名加Id,即驼峰风格。例如这里加了roleId的外键字段。

直到目前为止,CLI没有将任何东西插入数据库。 刚刚为模型 User和Role创建了必需的模型和迁移文件。 现在要在数据库中实际创建该表,需要运行 db:migrate 命令。

1
../node_modules/.bin/sequelize db:migrate

此命令将执行这些步骤:

  • 将在数据库中创建一个名为 SequelizeMeta 的表。 此表用于记录在当前数据库上运行的迁移
  • 开始寻找尚未运行的任何迁移文件。 这可以通过检查 SequelizeMeta 表。 在这个例子中,它将运行创建的 XXXXXXXXXXXXXX-create-role.js和XXXXXXXXXXXXXX-create-user.js 迁移。
  • 创建一个名为 Roles 的表,其中包含其迁移文件中指定的所有列。
  • 创建一个名为 Users 的表,其中包含其迁移文件中指定的所有列。

除了使用sequelize db:migrate命令创建数据库表之外,可以使用models同步创建或删除数据库表,例如:

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
if (appConfig.dbCreated === false) {
models.sequelize.sync().then(function() {
//保存应用配置
appConfig.dbCreated = true;
fs.writeFile(__dirname + '/../config/appConfig.json', JSON.stringify(appConfig), function(err) {
if (err) return console.error(err);
console.log('app config save is done');
});
Promise.all([
models.T_Role.create({ID:pwdHash.createMd5(Math.random().toString()),Role_Name:'管理员'}),
models.T_User.create({ID:pwdHash.createMd5(Math.random().toString()),Account:appConfig.adminAccount,Password:pwdHash.createHash(appConfig.adminPassword),Audit:true,Avatar:'/avatar/default.jpg'}),
models.T_Role.create({ID:pwdHash.createMd5(Math.random().toString()),Role_Name:'普通用户'})

]).then(function (result){
//console.log(result);
var role1=result[0];
var user1=result[1];
role1.setT_Users(user1).catch(err=>console.log(err));
}).catch(err=>console.log(err));


server.listen(port);
server.on('error', onError);
server.on('listening', onListening);
});
} else {
server.listen(port);
server.on('error', onError);
server.on('listening', onListening);
}

创建种子,生成测试数据

假设我们希望在默认情况下将一些数据插入到几个表中。 例如创建几个用户和角色:

1
2
../node_modules/.bin/sequelize seed:generate --name demo-role
../node_modules/.bin/sequelize seed:generate --name demo-user

这个命令将会在 seeders 文件夹中创建两个种子文件。文件名看起来像是 XXXXXXXXXXXXXX-demo-role.js和XXXXXXXXXXXXXX-demo-user.js,它遵循相同的 up/down 语义,如迁移文件。

现在我们应该编辑这两个文件,将演示角色插入Role表,将演示用户插入User表。修改XXXXXXXXXXXXXX-demo-role.js如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
'use strict';

module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.bulkInsert('Roles', [{
roleName: '管理员',
createdAt:new Date(),
updatedAt:new Date()
},{
roleName: '普通用户',
createdAt:new Date(),
updatedAt:new Date()
}], {});

},

down: (queryInterface, Sequelize) => {
return queryInterface.bulkDelete('Roles', null, {});

}
};

修改XXXXXXXXXXXXXX-demo-user.js如下:

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
'use strict';

module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.bulkInsert('Users', [{
roleId:1,
firstName: 'John',
lastName: 'Doe',
email: 'demo@demo.com',
createdAt:new Date(),
updatedAt:new Date()
},{
roleId:1,
firstName: 'Jack',
lastName: 'Smith',
email: 'jack@demo.com',
createdAt:new Date(),
updatedAt:new Date()
}], {});

},

down: (queryInterface, Sequelize) => {
return queryInterface.bulkDelete('Users', null, {});
}
};

种子文件修改后,即可使用如下命令将演示数据插入数据库中:

1
../node_modules/.bin/sequelize db:seed:all

工程实践经验总结

在工程实践中,只需使用sequelize-cli创建数据库,生成models即可,然后使用代码直接根据models创建数据库表,并生成测试数据。

参考链接

  1. Sequelize Reference,by sequelizejs.
  2. Sequelize 中文文档,by demopark.
  3. 解决Mysql中文乱码问题, by 最怕认真.
  4. How to set primary key type to UUID via Sequelize CLI,by stackoverflow.
  5. 数据库表主键设计方法,by jackhuang.
  6. Variable,by sequelize.
  7. cascade delete does not work,by sequelize.
  8. Adding foreign key to a table using Sequelize,by Nathan Sebhastian.
  9. Eager Loading - 预先加载,by demopark.

Visual_Studio_Community_2017离线安装

发表于 2018-06-21 | 更新于 2024-05-22

Visual Studio 2017版本采用模块化安装方式,根据用户选择模块按需安装。这种方式在用户能够上网的情况下十分方便,但在离线情况下就无法使用。下面介绍在离线情况下如何打包安装Visual Studio community 2017的方法。

下载VS2017社区版安装包获取程序

到官网下载最新版本的vs_community_xxxxxx.exe的下载器。

运行命令下载所需安装包

运行如下命令获取VS2017的工作负载:

1
.\vs_community__208176120.1522073957.exe --layout e:\vs2017_offline --lang zh-CN --add Microsoft.VisualStudio.Workload.CoreEditor --add Microsoft.VisualStudio.Workload.NativeDesktop --includeRecommended --includeOptional

可从参考链接[2]中获取工作负载ID。如需更多语言包或者更多安装指令,请参考官方离线安装命令指南,即参考链接[3]。

VS2017社区版 30天许可证过期解决方法

解决方法步骤如下:

  1. 外网电脑打开Vistual Studio Community2017。
  2. 在许可证过期弹窗中登陆即可自动下载许可证完成激活。许可证下载路径(C:\Users\user\AppData\Local\Microsoft\VSCommon\OnlineLicensing)
  3. 拷贝外网中的OnlineLicensing文件夹到内网电脑相应路径下(C:\Users\user\AppData\Local\Microsoft\VSCommon)
  4. 打开内网电脑Vistual Studio Community2017将不会再弹出许可证到期窗口。

VS2017控制台输出中文乱码问题

请参考:

  • VS2017控制台输出中文乱码 ,怎么设置编码格式(utf-8)?
  • 修改cmd控制台默认代码页编码的几种方法【GBK、UTF-8】
  • VS2017新建windows控制台程序打印中文乱码问题
  • VS2019控制台输出中文乱码问题已解决

VS2017中.ipch文件删除问题

.ipch文件和.sdf文件是Visual Studio用来保存预编译的头文件 和Intellisense 用的,删除这些文件对于工程的开发完全没有影响。具体方法请参考:

  • VisualStudio 产生的.ipch文件可以删除吗?

参考链接

  1. Create an offline installation of Visual Studio, by microsoft
  2. Visual Studio Community 组件目录, by microsoft
  3. Use command-line parameters to install Visual Studio, by microsoft
  4. Vistual Studio Community 2017 30天许可证过期,by 井底一蛤蟆.
  5. VS2017控制台输出中文乱码 ,怎么设置编码格式(utf-8)?,by 小凡1991.
  6. VisualStudio 产生的.ipch文件可以删除吗?,by Jacob-xyb.

Node_C++_Addon插件编写方法

发表于 2018-06-20 | 更新于 2019-10-03

最近研究在Nodejs中调用DLL,上网搜索发现主要有两种方法:

  • 使用Nodeffi调用C风格接口的DLL,但是无法调用C++风格导出类的DLL。
  • 使用Nodejs C++ Addon 插件,该方法可直接与C++代码交互,理论上可以调用C++风格导出类的DLL。

下面研究Nodejs Addon C++插件的编写方法。

什么是Nodejs C++ Addon

Node.js插件(Addons)是C/C++编写的动态链接对象,这些对象可以被Node.js的require()函数引用,并可以像普通的Node.js模块一样使用。Addons主要用于提供一个Node.js中运行的JavaScript和C/C++库之间的接口。

插件(Addons)是动态链接的共享对象,它提供了C/C++类库的调用能力。实现插件的方法比较复杂,涉及到以下元组件及API:

  • V8:C++库,Node.js用于提供JavaScript执行环境。V8提供了对象创建、函数调用等执行机制,V8相关API包含在了v8.h头文件中(位于Node.js源码树的deps/v8/include/v8.h),也可以查看在线文档。
  • libuv:C库,实现了Node.js中的事件循环、工作线程及在不同平台中异步行为的相关功能。也可以做为是一个跨平台的抽象库,提供了简单的、类POSIX的对主要操作系统的常见系统任务功能,如:与文件系统、套接字、计时器、系统事件的交互等。libuv还提供了一个类pthreads的线程池抽象对象,可用于更复杂的、超越标准事件循环的异步插件的控制功能。
  • 内部Node.js库:Node.js自身提供了一定义数量的C/C++API的插件可以使用 - 其中最重要的可能是node::ObjectWrap类
  • Node.js静态链接库:Node.js自身还包含了一部分静态链接库,如OpenSSL。这些位于Node.js源码树的deps/目录下,只有V8和OpenSSL提供了符号出口,可以供Node.js和基它插件所使用。详见Node.js依赖链接

Node Addon插件编写方法

Node Addon插件的编写需要解决两个关键问题:

  • 当数据流向 javaScript -> C++时,如何将javascript类型数据包装成C++类型数据,供C++代码使用。
  • 当数据流向 C++ -> JavaScript时,如何将C++类型数据包装成JavaScript类型数据,供JavaScript代码使用。

这两个关键问题的分析请参见淘宝前端团队成员发表的文章“Node.js 和 C++ 之间的类型转换[3]”。解决这两个关键问题后,Node Addon插件编写难度就不大了。

Node Addon插件调用C++导出类DLL方法测试

现有一个采用成熟方法导出类接口的DLL[4],如何在Node Addon插件中调用该DLL呢?下面nodejs官网Node Addon插件例子Factory of wrapped objects为例进行讲解。

Factory of wrapped objects例子在Addon插件中包装了一个MyObject类,现在就在MyObject类调用DLL导出类的接口方法。修改MyObject.h代码,增加DLL导出类接口方法,增加类接口成员变量IExport和DLL句柄变量hDll,如下所示:

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
34
35
36
// myobject.h
#ifndef MYOBJECT_H
#define MYOBJECT_H

#include <node.h>
#include <iostream>
#include <windows.h>
#include <node_object_wrap.h>
#include "MatureApproach.h"

namespace demo {
typedef IExport*(*TYPE_fnCreateExportObj) (void);//定义函数指针
typedef void(*TYPE_fnDestroyExportObj) (IExport*);//定义函数指针

class MyObject : public node::ObjectWrap {
public:
static void Init(v8::Isolate* isolate);
static void NewInstance(const v8::FunctionCallbackInfo<v8::Value>& args);

private:
explicit MyObject(double value = 0);
~MyObject();

static void New(const v8::FunctionCallbackInfo<v8::Value>& args);
static void PlusOne(const v8::FunctionCallbackInfo<v8::Value>& args);
static void Hi(const v8::FunctionCallbackInfo<v8::Value>& args);
static void Test(const v8::FunctionCallbackInfo<v8::Value>& args);
static v8::Persistent<v8::Function> constructor;
HMODULE hDll;
IExport* pExport;
double value_;
};

} // namespace demo

#endif

接下来将在MyObject类的构造函数中动态加载DLL,创建DLL导出类对象,在析构函数中析构DLL导出类对象,动态卸载DLL,在MyObject成员方法中调用DLL导出类方法,代码如下所示:

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
// myobject.cc
#include <node.h>
#include "myobject.h"

namespace demo {

using v8::Context;
using v8::Function;
using v8::FunctionCallbackInfo;
using v8::FunctionTemplate;
using v8::Isolate;
using v8::Local;
using v8::Number;
using v8::Object;
using v8::Persistent;
using v8::String;
using v8::Value;

Persistent<Function> MyObject::constructor;

MyObject::MyObject(double value) : value_(value){
MyObject::hDll = LoadLibrary("MatureApproach.dll"); //加载动态链接库DllDemo.dll文件;
TYPE_fnCreateExportObj fnCreateExportObj = (TYPE_fnCreateExportObj)GetProcAddress(MyObject::hDll, "CreateExportObj");
MyObject::pExport = fnCreateExportObj();
}

MyObject::~MyObject() {
TYPE_fnDestroyExportObj fnDestroyExportObj = (TYPE_fnDestroyExportObj)GetProcAddress(MyObject::hDll, "DestroyExportObj");
fnDestroyExportObj(MyObject::pExport);
FreeLibrary(MyObject::hDll);
}

void MyObject::Init(Isolate* isolate) {
// Prepare constructor template
Local<FunctionTemplate> tpl = FunctionTemplate::New(isolate, New);
tpl->SetClassName(String::NewFromUtf8(isolate, "MyObject"));
tpl->InstanceTemplate()->SetInternalFieldCount(3);

// Prototype
NODE_SET_PROTOTYPE_METHOD(tpl, "plusOne", PlusOne);
NODE_SET_PROTOTYPE_METHOD(tpl, "hi", Hi);
NODE_SET_PROTOTYPE_METHOD(tpl, "test", Test);

constructor.Reset(isolate, tpl->GetFunction());
}

void MyObject::New(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();

if (args.IsConstructCall()) {
// Invoked as constructor: `new MyObject(...)`
double value = args[0]->IsUndefined() ? 0 : args[0]->NumberValue();
MyObject* obj = new MyObject(value);
obj->Wrap(args.This());
args.GetReturnValue().Set(args.This());
} else {
// Invoked as plain function `MyObject(...)`, turn into construct call.
const int argc = 1;
Local<Value> argv[argc] = { args[0] };
Local<Function> cons = Local<Function>::New(isolate, constructor);
Local<Context> context = isolate->GetCurrentContext();
Local<Object> instance =
cons->NewInstance(context, argc, argv).ToLocalChecked();
args.GetReturnValue().Set(instance);
}
}

void MyObject::NewInstance(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();

const unsigned argc = 1;
Local<Value> argv[argc] = { args[0] };
Local<Function> cons = Local<Function>::New(isolate, constructor);
Local<Context> context = isolate->GetCurrentContext();
Local<Object> instance =
cons->NewInstance(context, argc, argv).ToLocalChecked();

args.GetReturnValue().Set(instance);
}

void MyObject::PlusOne(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();

MyObject* obj = ObjectWrap::Unwrap<MyObject>(args.Holder());
obj->value_ += 1;

args.GetReturnValue().Set(Number::New(isolate, obj->value_));
}

void MyObject::Hi(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();

MyObject* obj = ObjectWrap::Unwrap<MyObject>(args.Holder());
Local<String> str=v8::String::NewFromUtf8(isolate, obj->pExport->Hi().data());

args.GetReturnValue().Set(str);
}

void MyObject::Test(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();

MyObject* obj = ObjectWrap::Unwrap<MyObject>(args.Holder());
Local<String> str=v8::String::NewFromUtf8(isolate, obj->pExport->Test().data());

args.GetReturnValue().Set(str);
}

} // namespace demo

参考链接

  1. Node.js C/C++插件(Addons), by IT笔录
  2. Node.js v8.16.1 Documentation,by nodejs
  3. type-casts-between-node-and-cpp,by 淘宝前端团队
  4. DLL导出类和函数,by jackhuang

如何在Vistual_Studio中调试Node_Addon_C++插件

发表于 2018-06-18 | 更新于 2019-09-26

最近研究如何在Vistual Studio 2017社区版中调试Node addon C++插件,遇到很多问题,花费了不少时间,为防止遗忘,将方法记录下来。

准备工作

  1. 安装VS2017社区版
  2. 安装python 2.7
  3. 安装Nodejs 8.11.3 LTS
  4. 下载Nodejs 8.11.3 LTS源码

安装配置环境

  1. 安装node-gyp和nan
    node-gyp和nan用于编译生成node addon 插件。

    1
    2
    npm install -g node-gyp
    npm install -g nan
  2. 编译nodejs源码
    在nodejs源码解压后文件夹中执行如下命令:

    1
    .\vcbuild.bat debug x64
  3. 创建nodejs addon c++插件示例工程
    使用如下命名生成调试用的vs工程:

    1
    node-gyp configure rebuild --nodedir="D:\project\cpp_project\node-v8.11.3" --debug
  4. 修改测试用的index.js,使其调用上一步骤生成的调试用的插件

    //该js调用生成的插件
    var addon = require('./build/Debug/max');
    var y=addon.max(2,3);
    console.log(y);
  5. 使用VS2017打开插件示例工程的build文件夹下node-gyp生成的解决方案。右键单击工程,在上下文菜单中点击属性,配置其调试命令和命令参数,如下图所示。

    工程调试配置

  6. 设置断点进行调试

参考链接

  1. 如何在VS2015中搭建可以写node.js的C++Addon的环境, by Zmyths.
  2. how-to-debug-nodejs-addons-in-visual-studio,by cvtalks.
  3. Vistual Studio Community 2017 30天许可证过期,by 井底一蛤蟆.

DLL导出类和函数

发表于 2018-06-15 | 更新于 2022-07-27

最近研究在DLL中导出类,探寻最佳的DLL导出类的方法和技术。把整个过程记录一下,防止遗忘。

基础知识

动态链接库

动态链接库(英语:Dynamic-link library,缩写为DLL)是微软公司在微软视窗操作系统中实现共享函数库概念的一种实现方式。这些库函数的扩展名是.DLL、.OCX(包含ActiveX控制的库)或者.DRV(旧式的系统驱动程序)。

所谓动态链接,就是把一些经常会共用的代码(静态链接的OBJ程序库)制作成DLL档,当可执行文件调用到DLL档内的函数时,Windows操作系统才会把DLL档加载存储器内,DLL档本身的结构就是可执行档,当程序有需求时函数才进行链接。通过动态链接方式,存储器浪费的情形将可大幅降低。静态链接库则是直接链接到可执行文件。

编写方法

使用DLL导出C函数或全局变量很简单,具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 下列 ifdef 块是创建使从 DLL 导出更简单的
// 宏的标准方法。此 DLL 中的所有文件都是用命令行上定义的 DLLDEMO_EXPORTS
// 符号编译的。在使用此 DLL 的
// 任何其他项目上不应定义此符号。这样,源文件中包含此文件的任何其他项目都会将
// DLLDEMO_API 函数视为是从 DLL 导入的,而此 DLL 则将用此宏定义的
// 符号视为是被导出的。

#ifdef DLLDEMO_EXPORTS
#define DLLDEMO_API __declspec(dllexport)
#else
#define DLLDEMO_API __declspec(dllimport)
#endif

extern "C" extern DLLDEMO_API int nDllDemo;

//不使用extern "C"将导致函数名字改编
DLLDEMO_API int fnDllDemo(int);

extern "C" DLLDEMO_API int fnExternCDllDemo(int);

运行时通知DLL进程/线程加载

进程/线程加载时,可以通过DllMain函数通知DLL相关信息,提供对应处理的机会。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
BOOL WINAPI DLLMain(HINSTANCE hinstDLL,DWORD fdwReason,LPVOID fImpLoad)
{
switch(fdwReason)
{
case DLL_PROCESS_ATTACH:
//当这个DLL第一次被映射到了这个进程的地址空间时。DLLMain函数的返回值为FALSE,说明DLL的初始化没有成功,系统就会终结整个进程,去掉所有文件映象,之后显示一个对话框告诉用户进程不能启动。
break;
case DLL_THREAD_ATTACH:
//一个线程被创建,新创建的线程负责执行这次的DllMain函数。系统不会让进程已经存在的线程以DLL_THREAD_ATTACH的值来调用DllMain函数。主线程永远不会以DLL_THREAD_ATTACH的值来调用DllMain函数。系统是顺序调用DllMain函数的,一个线程执行完DllMain函数才会让另外一个线程执行DllMain函数。
break;
case DLL_THREAD_DETACH:
//如果线程调用了ExitThread来结束线程(线程函数返回时,系统也会自动调用ExitThread)。线程调用了TerminateThread,系统就不会用值DLL_THREAD_DETACH来调用所有DLL的DllMain函数。
break;
case DLL_PROCESS_DETACH:
//这个DLL从进程的地址空间中解除映射。如果进程的终结是因为调用了TerminateProcess,系统就不会用DLL_PROCESS_DETACH来调用DLL的DllMain函数。这就意味着DLL在进程结束前没有机会执行任何清理工作。
break;
}
return(TRUE);
}

DLL的静态调用方法

采用静态调用方法,DLL最终将打包到生成的EXE中。静态调用方法步骤如下[2]:

  1. 把你的youApp.DLL拷到你目标工程(需调用youApp.DLL的工程)的Debug目录下;
  2. 把你的youApp.lib拷到你目标工程(需调用youApp.DLL的工程)目录下;
  3. 把你的youApp.h(包含输出函数的定义)拷到你目标工程(需调用youApp.DLL的工程)目
    录下;
  4. 打开你的目标工程选中工程,选择Visual C++的Project主菜单的Settings菜单;
  5. 执行第4步后,VC将会弹出一个对话框,在对话框的多页显示控件中选择Link页。然
    后在Object/library modules输入框中输入:youApp.lib
  6. 选择你的目标工程Head Files加入:youApp.h文件;
  7. 最后在你目标工程(*.cpp,需要调用DLL中的函数)中包含你的:#include “youApp.h”

DLL的动态调用方法

动态调用DLL的步骤:

  1. 创建一个函数指针,其指针数据类型要与调用的DLL引出函数相吻合。
  2. 通过Win32 API函数LoadLibrary()显式的调用DLL,此函数返回DLL的实例句柄。
  3. 通过Win32 API函数GetProcAddress()获取要调用的DLL的函数地址, 把结果赋给自定义函数的指针类型。
  4. 使用函数指针来调用DLL函数。
  5. 最后调用完成后,通过Win32 API函数FreeLibrary()释放DLL函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int main()
{
HMODULE hModule = LoadLibrary(_T("DllDemo.dll"));
typedef int(*TYPE_fnDllDemo) (int);//定义函数指针
typedef int(*TYPE_fnExternCDllDemo) (int);//定义函数指针
//创建类对象
CDllDemo* pCDllDemo = (CDllDemo*)malloc(sizeof(CDllDemo));

TYPE_fnDllDemo fnDllDemo = (TYPE_fnDllDemo)GetProcAddress(hModule, "?fnDllDemo@@YAHH@Z");
int *nDllDemo = (int *)GetProcAddress(hModule, "nDllDemo");
TYPE_fnExternCDllDemo fnExternCDllDemo = (TYPE_fnExternCDllDemo)GetProcAddress(hModule, "fnExternCDllDemo");

if (pCDllDemo != NULL)
// printf("pCDllDemo->Max(32,42) = %d\n", pCDllDemo->Max(32, 42));//Dll导出类的调用太麻烦,因为DLL本来就是为C函数服务设计的。
if (fnDllDemo != NULL)
printf("fnDllDemo(32) = %d\n", fnDllDemo(32));
if (nDllDemo != NULL)
printf("*nDllDemo = %d\n", *nDllDemo);
if (fnExternCDllDemo != NULL)
printf("fnExternCDllDemo(22) = %d\n", fnExternCDllDemo(22));
_tsystem(_T("pause"));
FreeLibrary(hModule);
return 0;
}

COM技术

COM主要是一套给C/C++用的接口,当然为了微软的野心,它也被推广到了VB、Delphi以及其他一大堆奇奇怪怪的平台上。它主要为了使用dll发布基于interface的接口。我们知道dll的接口是为了C设计的,它导出的基本都是C的函数,从原理上来说,将dll加载到内存之后,会告诉你一组函数的地址,你自己call进去就可以调用相应的函数[3]。

但是对于C++来说这个事情就头疼了,现在假设你有一个类,我们知道使用一个类的第一步是创建这个类:new MyClass()。这里直接就出问题了,new方法通过编译器计算MyClass的大小来分配相应的内存空间,但是如果库升级了,相应的类可能会增加新的成员,大小就变了,那么使用旧的定义分配出来的空间就不能在新的库当中使用。

要解决这问题,我们必须在dll当中导出一个CreateObject的方法,用来代替构造函数,然后返回一个接口。然而,接口的定义在不同版本当中也是有可能会变化的,为了兼容以前的版本同时也提供新功能,还需要让这个对象可以返回不同版本的接口。接口其实是一个只有纯虚函数的C++类,不过对它进行了一些改造来兼容C和其他一些编程语言。

在这样改造之后,出问题的还有析构过程~MyClass()或者说delete myClass,因为同一个对象可能返回了很多个接口,有些接口还在被使用,如果其中一个被人delete了,其他接口都会出错,所以又引入了引用计数,来让许多人可以共享同一个对象。

其实到此为止也并不算是很奇怪的技术,我们用C++有的时候也会使用Factory方法来代替构造函数实现某些特殊的多态,也会用引用计数等等。COM技术的奇怪地方在于微软实在是脑洞太大了,它们构造了一个操作系统级别的Factory,规定所有人的Interface都统一用UUID来标识,以后想要哪个Interface只要报出UUID来就行了。这样甚至连链接到特定的dll都省了。

这就好比一个COM程序员,只要他在Windows平台上,调用别的库就只要首先翻一下魔导书,查到了一个用奇怪文字写的“Excel = {xxx-xxx-xxxx…}”的记号,然后它只要对着空中喊一声:“召唤,Excel!CoCreateInstance, {xxx-xxx-xxxx…}”然后呼的从魔法阵里面窜出来了一个怪物,它长什么样我们完全看不清,因为这时候它的类型是IUnknow,这是脑洞奇大无比的微软为所有接口设计的一个基类。

我们需要进一步要求它变成我们能控制的接口形态,于是我们再喊下一条指令:“变身,Excel 2003形态!QueryInterface, {xxx-xxx-xxxx…}”QueryInterface使用的是另一个UUID,用来表示不同版本的接口。于是怪物就变成了我们需要的Excel 2003接口,虽然我们不知道它实际上是2003还是2007还是更高版本。等我们使唤完这只召唤兽,我们就会对它说“回去吧,召唤兽!Release!”但是它不一定听话,因为之前给它的命令也许还没有执行完,它会忠诚地等到执行完再回去,当然我们并不关心这些细节。

微软大概会觉得自己设计出了软件史上最完美的二进制接口,从今以后所有的第三方库都可以涵盖在这套接口之下。然而历史的车轮是无情的,它碾过那些自以为是的人的速度总是会比想象的更快。Java的直接基于类的接口被广泛应用,开发使用起来远远来的简单,即便偶尔出点问题大家也都想办法解决了,事实证明程序员并不愿意花10倍的编写代码的时间来解决二进制库的版本兼容问题,他们更愿意假装没看见。很快微软也抄了一个.NET托管dll的方案出来,于是纯的二进制接口COM就慢慢被抛弃了。

COM,OLE,ActiveX,OCX,VBScript,历史不会忘记你们的,如果历史忘了,我替历史记住你们。安息吧。

DLL导出类

借鉴COM技术,这里直接给出DLL到处类的成熟方法,可有效避免DLL地狱问题。具体结构为:

导出类是一个派生类,派生自一个抽象类——都是纯虚函数。使用者需要知道这个抽象类的结构。DLL最少只需要提供一个用于获取类对象指针的接口。使用者跟DLL提供者共用一个抽象类的头文件,使用者依赖于DLL的东西很少,只需要知道抽象类的接口,以及获取对象指针的导出函数,对象内存空间的申请是在DLL模块中做的,释放也在DLL模块中完成,最后记得要调用释放对象的函数。

这种方式比较好,通用,产生的DLL没有特定环境限制。借助了C++类的虚函数。一般都是采用这种方式。除了对DLL导出类有好处外,采用接口跟实现分离,可以使得工程的结构更清晰,使用者只需要知道接口,而不需要知道实现。

具体代码如下:

  1. DLL导出类

    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
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    //DLL导出类头文件MatureApproach.h,与DLL使用者共享
    #ifdef MATUREAPPROACH_EXPORTS
    #define MATUREAPPROACH_API __declspec(dllexport)
    #else
    #define MATUREAPPROACH_API __declspec(dllimport)
    #endif

    class IExport
    {
    public:
    virtual void Hi() = 0;
    virtual void Test() = 0;
    virtual void Release() = 0;
    };


    extern "C" MATUREAPPROACH_API IExport* _stdcall CreateExportObj();
    extern "C" MATUREAPPROACH_API void _stdcall DestroyExportObj(IExport* pExport);

    //DLL导出接口函数的实现MatureApproach.cpp
    #include "stdafx.h"
    #include "MatureApproach.h"
    #include "ExportClassImpl.h"

    BOOL APIENTRY DllMain( HMODULE hModule,
    DWORD ul_reason_for_call,
    LPVOID lpReserved
    )
    {
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
    break;
    }
    return TRUE;
    }

    MATUREAPPROACH_API IExport* APIENTRY CreateExportObj()
    {
    return new ExportImpl;
    }


    //这里不能直接delete pExport,因为没有把IExport的析构函数定义为虚函数
    MATUREAPPROACH_API void APIENTRY DestroyExportObj(IExport* pExport)
    {
    pExport->Release();
    }
  2. DLL导出类的具体实现

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
34
35
36
//DLL导出类头文件ExportClassImpl.h
#include "MatureApproach.h"

class ExportImpl : public IExport
{
public:
virtual void Hi();
virtual void Test();
virtual void Release();
~ExportImpl();
private:
};

//DLL导出类的实现ExportClassImpl.cpp
#include "stdafx.h"
#include "ExportClassImpl.h"

void ExportImpl::Hi()
{
wcout << L"Hello World" << endl;
}

void ExportImpl::Test()
{
wcout << L"Hi cswuyg" << endl;
}

void ExportImpl::Release()
{
delete this;
}

ExportImpl::~ExportImpl()
{
cout << "Release OK" << endl;
}

Dll导出C++类的3种方式

Using pure C (纯C语言方式)

这种方式类似与Win32的窗口句柄,用户将句柄作为参数传递给函数,并对对象执行各种操作。存在以下缺点:

  1. 调用创建对象函数的时候编译器无法判断类型是否匹配
  2. 需要手动调用Release函数,一旦忘记则会造成内存泄露
  3. 如果导出的函数的参数支持除基本数据类型以外的其他类型的参数(例如:class),则也得为这些类型提供接口。

Using a regular C++ class (C++直接导出类)

缺点:

  1. 这种方式虽然简单易用,但是局限性很大,而且后期维护会很麻烦,除了导出的东西太多、使用者对类的实现依赖太多之外,还有其它问题:必须保证使用同一种编译器。导出类的本质是导出类里的函数,因为语法上直接导出了类,没有对函数的调用方式、重命名进行设置,导致了产生的dll并不通用。
  2. Dll地狱问题:

Using an abstract C++ interface (使用抽象接口方式)

C++抽象接口(仅包含纯虚函数且不包含数据成员的C++类)同时兼顾以下两个方面:与对象无关的纯净接口,以及方便的的面向对象的调用方式。

推荐使用该种方式导出类库。

优点:

  • 导出的C++类可以通过抽象接口与任何C++编译器一起使用。
  • DLL的C运行时库和客户端彼此独立。因为资源获取和释放完全发生在DLL模块内部,客户端不受DLL内部改变的影响。
  • 实现了真正的模块分离。可以重新设计和重建生成的DLL模块,而不会影响项目的其余部分。
  • 如果需要,可以将DLL模块轻松转换为成熟的COM模块。

缺点:

  • 创建新对象实例并将其删除需要显式函数调用。但是,智能指针可以解决。

  • 抽象接口方法不能返回或接受常规C++对象作为参数。它是A内置类型(如int,double,char*等)或另一抽象接口。它与C接口的限制相同。

参考链接

  1. 动态链接库, by wikipedia
  2. C++调用DLL有两种方法——静态调用和动态调用,by 特洛伊-Micro
  3. 怎么通俗的解释COM组件?, by 灵剑.
  4. 编写DLL所学所思(2)——导出类,by 烛秋.
  5. Visibility,by Niall Douglas.
  6. Linux 編譯 shared library 的方法和注意事項,by fcamel.
  7. GCC制作Library–shared部分相当不错,by kk.
  8. [原创]Dll导出C++类的3种方式(多干货) ,by Jmsrwt.
  9. C++类库开发详解,by 奔跑的小河.
  10. C++ DLL导出类 知识大全,by 三小.
  11. std::vector needs to have dll-interface to be used by clients of class ‘X warning,by stackoverflow.
上一页1…49505152下一页

Jack Huang

519 日志
66 标签
© 2025 Jack Huang
由 Hexo 强力驱动
|
主题 — NexT.Muse