当前位置: 棋牌电玩游戏平台 > 新闻中心 > 正文

structc 开源框架介绍

时间:2019-08-26 12:20来源:新闻中心
引言 - 一时心起, libuv linux 搭建 引言 - 一切才刚刚开始 在前面几篇文章介绍到v8,addon,libuv等知识后,现在终于可以有信心看node的源码了,对一个软件来说,启动和关闭是短暂的,但

引言 - 一时心起, libuv linux 搭建

引言 - 一切才刚刚开始

在前面几篇文章介绍到v8,addon,libuv等知识后,现在终于可以有信心看node的源码了,对一个软件来说,启动和关闭是短暂的,但又是整个软件架构很关键的地方,一个设计良好的软件:在启动的时候快速稳定;运行的时候内存无泄漏,cup占用稳定有规律,服务可靠有包装;关闭的时候无错误。我们看看node是否认真考虑了这些。

  有一天突然想起来想写个动画. 找了一下 ui 库太大. 后面想起以前弄过的 libuv. 但发现 libuv 相关资料也很少.

  structc 是 C 结构基础库. 简单可复用.

准备

我是在windows下面用visual studio看代码的,先要做好准备工作:

  1. 下载node代码:https://github.com/nodejs/node.git
  2. 切换到自己想看的branch,我看的是v0.11.13
  3. 生成vs工程文件,详细步骤可以看BUILDING文档
  4. 打开工程,linux用户体会不到这种快感
![](https://upload-images.jianshu.io/upload_images/1616121-a65f3e16fcc20022.png)

vs打开node

所以就有了这些内容. 

  structc -

运行

可以用脚本编译,也可以用visual studio运行。

  1. 上面的第三部使用vcbuild nosign,就会编译完,生成node.exe。

    图片 1

    生成node.exe

  2. 通过visual studio运行

![](https://upload-images.jianshu.io/upload_images/1616121-3bab06f7043c807b.png)

vs运行node

node和我们平时写的程序也是一样生成exe文件,呵呵,感觉也没有那么神秘了。下面我们去看启动代码吧。node可是集成了v8和libuv,应该是蛮复杂的吧。

  libuv -

  之前也描述过几次 structc, 文字多代码风格少. 最近加班不多, 准备详细解说哈其思考初衷.

入口

代码这么多,怎么找到入口呢。幸亏我们有IDE,直接单步调试(F11)好了。发现代码在wmain停了下来。

图片 2

找到入口

虽然大部分软狗对此IDE使用场景很熟悉,但不排除还有一大部分开发linux系统的人还在用命令行和vim来看代码,实际上linux上面的用户可以尝试一下Jetbrain的IDE

libuv 在 linux 上面使用比较简单,  一开始 从 linux hello 跑起来

0.0 整体结构

启动

由于代码有大量宏来处理跨平台差异和简化代码,下面的阅读不会关注这些宏,主要看如何启动libuv,v8和node内置模块的加载;文章也不会解释libuv和v8的相关api,因为前面有文章介绍过了。

好了,从node.cc的int Start(int argc, char** argv) {函数慢慢看吧。

libuv linux 安装

structc├── extern├── LICENSE├── Makefile├── README.md├── structc└── structc.sln

初始化参数

在Start函数中,第一个调用的函数是Init,从名字来看便略知一二。

void Init(int* argc,
          const char** argv,
          int* exec_argc,
          const char*** exec_argv) {

这个函数处理一些初始化的工作,解析用户传入的参数,设置debug的相关的信息。这个版本的代码v8和libuv是混在一起的,在我看来是需要重构的。可见外国高手写代码也是先码功能。

首先假定你和我一样用的是Ubuntu去做开发. 在云平台上面测试过, Ubuntu Server 版本比 CentOS 版本少个十几兆.

structc.sln : winds 项目管理文件visual studio

初始化v8

下面的代码说明,在启动libuv循环前,先给v8实例node_isolate加了一个锁。这样保证当前node线程才能使用v8

V8::Initialize();
  {
    Locker locker(node_isolate);
    Environment* env =
        CreateEnvironment(node_isolate, argc, argv, exec_argc, exec_argv);
    // This Context::Scope is here so EnableDebug() can look up the current
    // environment with Environment::GetCurrentChecked().
    // TODO(bnoordhuis) Reorder the debugger initialization logic so it can
    // be removed.
    {
      Context::Scope context_scope(env->context());

CreateEnvironment创建了一个process对象(在JavaScript中),完成了进程相关信息的保存和一些全局设置。我们再测试一下process对象到底有什么:

图片 3

process对象

可见,我们如果想知道当前node一些全局的信息比如版本,可以通过process对象拿到。
另外,CreateEnvironment调用Load加载src/node.js,后面再看。

有兴趣朋友可以详细比较数据, 也可以尝试跑跑 Ubuntu Server .

structc : 项目整体源码和素材文件目录

初始化libuv循环

通过下面代码,我们可以看到:

  1. uv_run启动事件循环
  2. 设置循环模式为UV_RUN_ONCE,这样node会自动停止(如果没有要处理的handle的话)
  3. EmitBeforeExit调用env中回调函数emit。
  4. 如果还有新产生的需要处理的事物,继续循环。
    注意:这里又调用一次UV_RUN_NOWAIT,可能是因为uv_run比uv_loop_alive有更多语义,这里即用uv_loop_alive,又用UV_RUN_NOWAIT,至少代码不够清晰,可以考虑重构一下。
 do {
        more = uv_run(env->event_loop(), UV_RUN_ONCE);
        if (more == false) {
          EmitBeforeExit(env);

          // Emit `beforeExit` if the loop became alive either after emitting
          // event, or after running some callbacks.
          more = uv_loop_alive(env->event_loop());
          if (uv_run(env->event_loop(), UV_RUN_NOWAIT) != 0)
            more = true;
        }
      } while (more == true);
# libuv 安装
cd
wget https://github.com/libuv/libuv/archive/v1.18.0.tar.gz
tar -zxvf v1.18.0.tar.gz
cd libuv-1.18.0

sh autogen.sh
./configure

make -j4

sudo make install
sudo ldconfig
cd ../
rm -rf libuv-1.18.0 v1.18.0.tar.gz
```

README.md : 项目介绍 Markdown

加载Node.js

前面说到CreateEnvironment调用Load加载src/node.js。现在我们看看node.js有哪些功能。同时,node.js又加载了一些native模块,我们看看这互相加载到底怎么弄的。

  • CreateEnvironment函数
Environment* CreateEnvironment(Isolate* isolate,
                               int argc,
                               const char* const* argv,
                               int exec_argc,
                               const char* const* exec_argv) {
......

  Load(env);

......

  • load函数
void Load(Environment* env) {
 HandleScope handle_scope(env->isolate());

  // Compile, execute the src/node.js file. (Which was included as static C
  // string in node_natives.h. 'natve_node' is the string containing that
  // source code.)

......

  Local<String> script_name = FIXED_ONE_BYTE_STRING(env->isolate(), "node.js");
  Local<Value> f_value = ExecuteString(env, MainSource(env), script_name);

  Local<Function> f = Local<Function>::Cast(f_value);
......
}

我们注意一下ExecuteString干了什么


  • ExecuteString
// Executes a str within the current v8 context.
static Local<Value> ExecuteString(Environment* env,
                                  Handle<String> source,
                                  Handle<String> filename) {
......
  Local<v8::Script> script = v8::Script::Compile(source, filename);
......

  Local<Value> result = script->Run();
  if (result.IsEmpty()) {
    ReportException(env, try_catch);
    exit(4);
  }

  return scope.Escape(result);
}

调试的时候, Handle<String>并不能在调试器看到值,可以用下面的代码打印一下

  v8::String::Utf8Value param1(script_name);

或者下载Visual Studio Debugger Visualizers

git clone https://chromium.googlesource.com/chromium/src/tools/win

上面是chrome的调试工具,调试v8的时候好像不好用,


  • v8::Script::Compile(source, filename);
    这里进入v8编译JavaScript环节了。不在往下挖掘,因为这样对分析Node的启动没有什么好处。我们还是去看看node.js做了什么。

执行上面命令操作, 我们的系统中就已经有了 libuv 开发环境.

Makefile : linux 编译文件make

Node.js文件

我们看看node.js文件头部。

// Hello, and welcome to hacking node.js!
//
// This file is invoked by node::Load in src/node.cc, and responsible for
// bootstrapping the node.js core. Special caution is given to the performance
// of the startup process, so many dependencies are invoked lazily.

(function(process) {
  this.global = this;

......

});
  1. 定义了一个函数。
  2. 在src/node.cc中被node::Load调用。
  3. 为了加快速度,很多依赖都延迟加载了。

我们研究一下他加载了什么。

  • 第一部分: startup
    node.js定义了一个startup函数并调用,startup函数中使用NativeModule去加载很多模块
(function(process) {
  this.global = this;

  function startup() {
   ......
  }

  startup.globalVariables = function() {
  ......
  };

  startup();

  • 第二部分:定义NativeModule的加载机制
  // Below you find a minimal module system, which is used to load the node
  // core modules found in lib/*.js. All core modules are compiled into the
  // node binary, so they can be loaded faster.

  var ContextifyScript = process.binding('contextify').ContextifyScript;
  function runInThisContext(code, options) {
    var script = new ContextifyScript(code, options);
    return script.runInThisContext();
  }

  function NativeModule(id) {
    ......
  }

......

对于NativeModule,我们要仔细看看,到底是怎么加载的。。。

有一点需要注意的是当我们要使用 libuv时候推荐用静态库.

LICENSE : MIT 开源协议

NativeModule

  • 什么是native模块

这里的native并不是c 代码,而是js,从图中可以看到node自带了很多js。为了加快加载速度,把这些js通过一个python工具转成了node_natives.h这个头文件,然后直接编译到node.exe中。

图片 4

native js

图片 5

编译选项

从代码中可以看到node.js也被放到头文件了。


  • NativeModule如何加载模块

1. 导入natives

 NativeModule._source = process.binding('natives');

这个natives就是在头文件中定义的数据。
process.binding实在node.cc中定义的:NODE_SET_METHOD(process, "binding", Binding);。上面看到的自带的js大量使用这个函数加载C 模块。

2. NativeModule.require函数
js都是使用require函数来加载模块,只不过这个require也是普通的函数而已,并不是语言本身支持的。我们看看代码。

NativeModule.require = function(id) {
    if (id == 'native_module') { //在module模块还会require('native_module')
      return NativeModule;
    }

    var cached = NativeModule.getCached(id);
    if (cached) {
      return cached.exports;
    }

    if (!NativeModule.exists(id)) {
      throw new Error('No such native module '   id);
    }

    process.moduleLoadList.push('NativeModule '   id);

    var nativeModule = new NativeModule(id);

    nativeModule.cache();
    nativeModule.compile();

    return nativeModule.exports;
  };

3. compile函数
这里看一下compile函数。实际上只是在第一步的数组中查找。

NativeModule.prototype.compile = function() {
    var source = NativeModule.getSource(this.id);
    source = NativeModule.wrap(source);

    var fn = runInThisContext(source, { filename: this.filename });
    fn(this.exports, NativeModule.require, this, this.filename);

    this.loaded = true;
  };

wrap就是包装了一个函数

NativeModule.wrap = function(script) {
    return NativeModule.wrapper[0]   script   NativeModule.wrapper[1];
  };

  NativeModule.wrapper = [
    '(function (exports, require, module, __filename, __dirname) { ',
    'n});'
  ];

接下来看一下runInThisContext

4. runInThisContext

var ContextifyScript = process.binding('contextify').ContextifyScript;
  function runInThisContext(code, options) {
    var script = new ContextifyScript(code, options);
    return script.runInThisContext();
  }

node.js文件中的js代码只是调用了C ,我们得看一下C 代码。

 static void RunInThisContext(const FunctionCallbackInfo<Value>& args) {
    Isolate* isolate = args.GetIsolate();
    HandleScope handle_scope(isolate);

    // Assemble arguments
    TryCatch try_catch;
    uint64_t timeout = GetTimeoutArg(args, 0);
    bool display_errors = GetDisplayErrorsArg(args, 0);
    if (try_catch.HasCaught()) {
      try_catch.ReThrow();
      return;
    }

    // Do the eval within this context
    Environment* env = Environment::GetCurrent(isolate);
    EvalMachine(env, timeout, display_errors, args, try_catch);
  }

EvalMachine不看了,v8运行代码了。


gcc -l:libuv.a

extern : 项目引入的外部库目录

总结

上面主要的逻辑都在CreateEnvironmentnode.js中,从c 掉用到js,再从js调用c ,js调用js。着实复杂。

启动过程大致如下:

  1. 初始化v8
  2. 创建process对象
  3. 加载node.js
  4. node.js reqiure更多模块
  5. native模块加载完毕
  6. 如果传入文件,比如node myIndex.js,加载用户模块(startup函数中处理)
  7. libuv循环建立
  8. 等待或者结束(根据启动参数不同)

我们可以看到为了加快native模块的加载速度,采用了把js编译成.h文件的方法,我们如果想加快启动速度也可以这么干。

如此粗糙的过程虽然不能完全了解到node启动的过程,很多细节有带进一步研究,但是我们至少又前进了一些。_

本文参考nodejs-source-reading-note

到这里 linux 安装 libuv 已经完工了. 

extern├── jemalloc├── jemalloc-vc141-Release-static.lib├── libuv.lib├── pthread.h├── pthread_lib.lib├── sched.h├── semaphore.h├── strings.h├── uv└── uv.h

  不妨写个 hello world demo

以上就是我们看到 structc 项目整体结构.

#include <uv.h>
#include <assext.h>

//
// 测试 libuv tty 操作控制台
// 输出一段有颜色的文字
//
void uv_tty_test(void) {
    uv_tty_t tty;
    uv_buf_t buf[3];
    unsigned i, len = sizeof buf / sizeof *buf;
    uv_loop_t * loop = uv_default_loop();

    // 目前只对 tty 控制台处理
    if (uv_guess_handle(1) != UV_TTY) {
        fprintf(stderr, "uv_guess_handle(1) != UV_TTY!n");
        exit(EXIT_FAILURE);
    }

    uv_tty_init(loop, &tty, 1, 0);
    uv_tty_set_mode(&tty, UV_TTY_MODE_NORMAL);

    // 开始发送消息
    buf[0].base = "\033[46;37m";
    buf[1].base = u8"(✿◡‿◡) 喵酱 ((●'-'●)) 比 ♥ 里~ n";
    buf[2].base = "\033[0m";
    for (i = 0; i < len;   i)
        buf[i].len = (int)strlen(buf[i].base);
    uv_try_write((uv_stream_t *)&tty, buf, len);

    // 重置终端行为
    uv_tty_reset_mode();
    uv_run(loop, UV_RUN_DEFAULT);
}

0.1 外部库

代码运行效果是, 输出一段话, 并且设置背景色.  对于  uv_tty_test 可以理解为 main (本质是 structc 一种单元测试函数约束写法)

  当前很谨慎的引入两个半外部库. 最大程度会静态库编译链接运行. 荣我慢慢细说.

到这容我安利一个小东西, 感兴趣的可以尝试一下, 从零开始搭建一个 c 的 struct 小框架. 五脏逐渐全了.

  1. jemalloc -

  structc -

  jemalloc 是 c 构建底层高性能 malloc 库. 也被称为系统编程末期最后免费午餐.整个structc

简单说一下libuv中使用的几个函数,  第一个是 uv_try_write 尝试立即发送消息数组. 不像 uv_write 写入到消息队列中.

malloc全权交给 je_malloc 抗压.其中 winds 编译静态库部分, 项目本身也有细说 -

int uv_try_write(uv_stream_t* handle, const uv_buf_t bufs[], unsigned int nbufs)

    Same as uv_write(), but won’t queue a write request if it can’t be completed immediately.
    Will return either:
        > 0: number of bytes written (can be less than the supplied buffer size).
        < 0: negative error code (UV_EAGAIN is returned if no data can be sent immediately).

目前我们是用 tty 输出到屏幕上面, 可以用这个 api . 如果单纯是走 TCP, 不要过于依赖这个 api.

How to build jemalloc for Windows=================================1. Install Cygwin with at least the following packages:   * autoconf   * autogen   * gawk   * grep   * sed2. Install Visual Studio 2015 or 2017 with Visual C  3. Add Cygwinbin to the PATH environment variable4. Open "x64 Native Tools Command Prompt for VS 2017"   (note: x86/x64 doesn't matter at this point)5. Generate header files:   sh -c "CC=cl ./autogen.sh"6. Now the project can be opened and built in Visual Studio:   msvcjemalloc_vc2017.sln

说白了为了稳定性还是别用 uv_try_write.

( 注: vs 使用最新版本. 网址打不开那就FQ. 后面其也一样, 时刻保证最新 2018/10/10 ~ )

第二个要说的是 uv_run

对于 linux 编译安装参照下面脚本

int uv_run(uv_loop_t* loop, uv_run_mode mode)

    This function runs the event loop. It will act differently depending on the specified mode:
        UV_RUN_DEFAULT: Runs the event loop until there are no more active and referenced handles or requests. 
              Returns non-zero if uv_stop() was called and there are still active handles or requests.
               Returns zero in all other cases.
        UV_RUN_ONCE: Poll for i/o once. Note that this function blocks if there are no pending callbacks. 
             Returns zero when done (no active handles or requests left), 
             or non-zero if more callbacks are expected 
             (meaning you should run the event loop again sometime in the future).
        UV_RUN_NOWAIT: Poll for i/o once but don’t block if there are no pending callbacks. 
              Returns zero if done (no active handles or requests left), 
              or non-zero if more callbacks are expected 
              (meaning you should run the event loop again sometime in the future).
# 开发环境安装sudo apt install gcc gdb autogen autoconf# jemalloc 安装cdwget https://github.com/jemalloc/jemalloc/releases/download/5.1.0/jemalloc-5.1.0.tar.bz2tar -jxvf jemalloc-5.1.0.tar.bz2cd jemalloc-5.1.0sh autogen.shmake -j4sudo make installsudo ldconfigcdrm -rf jemalloc-5.1.0 jemalloc-5.1.0.tar.bz2

其中 UV_RUN_DEFAULT 表示 uv_run 会一直阻塞运行, 只到没有事情要处理的时候, 才会有返回值.

当 jemalloc 构建好了. 设计 alloc 层引入到 structc 框架中, 用户取代系统 malloc...

而 UV_RUN_ONCE 表示执行 poll 一次. 类比你写代码只调用一次 select 阻塞, 直到事件激活或者超时触发.

alloc.h -

相似的 UV_RUN_NOWAIT 也是只 poll 轮询一次, 但是没有要处理事情是不会阻塞.

#ifndef _H_ALLOC#define _H_ALLOC#include <stdlib.h>#include <string.h>// :) 高效内存分配, 莫名伤感 ~// _MSC_VER -> Winds CL// __GNUC__ -> Linux GCC//#ifdef _MSC_VER//// CPU 检测 x64 or x86// ISX64 defined 表示 x64 否则 x86//#   if defined || defined#       define ISX64#   endif//// _M_PPC 为 PowerPC 平台定义, 现在已不支持// so winds 可以认为都是小端平台//#   if defined#       define ISBENIAN#   endif#elif  __GNUC__#   if defined(__x86_64__)#       define ISX64#   endif//// 大小端检测 : ISBENIAN defined 表示大端//#   if defined(__BIG_ENDIAN__) || defined(__BIG_ENDIAN_BITFIELD)#       define ISBENIAN#   endif#else#   error BUILD  S#endif// OFF_ALLOC - 关闭全局 free / malloc 配置#ifndef OFF_ALLOC#   undef  free#   define free    free_#   undef  strdup#   define strdup  strdup_#   undef  malloc#   define malloc  malloc_#   undef  calloc#   define calloc  calloc_#   undef  realloc#   define realloc realloc_#endif//OFF_ALLOC//// free_ - free 包装函数// ptr      : 内存首地址// return   : void//extern void free_(void * ptr);//// malloc_ - malloc 包装, 封装一些特殊业务// size     : 分配的内存字节// return   : 返回可使用的内存地址.//extern void * malloc_(size_t size);//// strdup_ - strdup 包装函数// s        : '\0' 结尾 C 字符串// return   : 拷贝后新的 C 字符串//extern char * strdup_(const char * s);//// calloc_ - calloc 包装, 封装一些特殊业务// num      : 数量// size     : 大小// return   : 返回可用内存地址, 并且置0//extern void * calloc_(size_t num, size_t size);//// realloc_ - realoc 包装函数, 封装一些特殊业务// ptr      : 内存首地址, NULL 等同于 malloc// size     : 重新分配的内存大小// return   : 返回重新分配好的新地址内容//extern void * realloc_(void * ptr, size_t size);#endif//_H_STDEXIT

  到这里, 差不多 linux libuv 的 hello world 应该也算起来了.

alloc.c -

 

#include <stdio.h>#define OFF_ALLOC#include "alloc.h"#define JEMALLOC_NO_DEMANGLE#include <jemalloc/jemalloc.h>//// free_ - free 包装函数// ptr      : 内存首地址// return   : void//inline void free_(void * ptr) {    je_free;}// 简单内存不足检测处理static inline void * mcheck(void * ptr, size_t size) {    if (NULL == ptr) {        fprintf(stderr, "out of memory trying to allocate %zun", size);        fflush;        abort();    }    return ptr;}//// malloc_ - malloc 包装, 封装一些特殊业务// size     : 分配的内存字节// return   : 返回可使用的内存地址.//inline void * malloc_(size_t size) {    void * ptr = je_malloc;    return mcheck(ptr, size);}//// strdup_ - strdup 包装函数// s        : '\0' 结尾 C 字符串// return   : 拷贝后新的 C 字符串//inline char * strdup_(const char * s) {    if  {        size_t n = strlen   1;        char * ptr = malloc_;        return memcpy(ptr, s, n);    }    return NULL;}//// calloc_ - calloc 包装, 封装一些特殊业务// num      : 数量// size     : 大小// return   : 返回可用内存地址, 并且置0//inline void * calloc_(size_t num, size_t size) {    void * ptr = je_calloc(num, size);    return mcheck(ptr, size);}//// realloc_ - realoc 包装函数, 封装一些特殊业务// ptr      : 内存首地址, NULL 等同于 malloc// size     : 重新分配的内存大小// return   : 返回重新分配好的新地址内容//inline void * realloc_(void * ptr, size_t size) {    void * ntr = je_realloc(ptr, size);    return mcheck(ntr, size);}

前言 - winds 跑起 libuv

包装了一层. 从 alloc.h 中 OFF_ALLOC 宏可以看出, 具备支持插拔能力 ~

   下面开始带大家, 在 winds 编译最新版本 libuv.  同样在 github 上 下载 libuv 最新的发布版本.

2.libuv -  

    libuv-1.18.0

  libuv 用 c 写的高性能单线程网络 io 库. 希望通过它来支撑网络层.winds 编译静态库

参照 libuv 项目首页燥起来就行. 其中 gyp 安装了这个版本, 其它随波逐流 ~

解压操作完成后, 会是下面这样的

  gyp -

图片 6

linux 编译安装脚本

这时候先参照一下官网的 libuv 首页 README.md 说明.  

# libuv 安装cdwget https://github.com/libuv/libuv/archive/v1.23.1.zipunzip v1.23.1.zipcd libuv-1.23.1sh autogen.sh./configuremake -j4sudo make installsudo ldconfigcd## 注意 uv 头文件, 全部导入到系统 include 目录下面#rm -rf libuv-1.23.1 v1.23.1.zip

先安装 Python 2.7 . 扯一点.  最近 python 好虎 (2017年12月23日),  但是还是不理解为啥 2.7 和 3.x 版本不兼容. 

注意要将编译后 include 完整拷贝到安装目录 include下. 这样 uv 头文件全, 日后会用到.

就目前而言还是多用 Python 2.7 感觉.  随后安装 gyp google 推出的跨平台编译环境.

libuv 开箱即用, 不太需要什么基础封装.

  gyp - 

3.pthread -

由于使用的是 VS2017, 原始版本 gyp 不支持, 请参照我提的这个提交, 进行修改让其支持 VS2017 版本

  这是最后那半个, 为 winds 引入 POSIX thread 模型.编译起来很简单(前提咱们 VS 玩的熟).

  gyp-vs2017 version  - 

扯点闲篇. linux 和 winds 相辅相成, 对立而统一. 一个是一切从头码, 一个开始就已经注册未来.

ok winds 10 VS2017 libuv-1.18.0 python2.7 gyp gyp vs2017 version 编译环境搭建完毕.

描述比较粗, 但大概这意思. (两个都不 eary, 玩很久才敢入门见岳父岳母) . 这里包装了一层

编辑:新闻中心 本文来源:structc 开源框架介绍

关键词: libuv winds linux tty 开源