2024年1月

Ref & ShallowRef

ref:
接受一个参数值并返回一个响应式且可改变的 ref 对象。ref 对象拥有一个指向内部值的单一属性 .value

可以将 ref 看成 reactive 的一个变形版本,这是由于 reactive 内部采用 Proxy 来实现,而 Proxy 只接受对象作为入参,这才有了 ref 来解决值类型的数据响应,
如果传入 ref 的是一个对象,内部也会调用 reactive 方法进行深层响应转换

const count = ref(0)
console.log(count.value) // 0

count.value++
console.log(count.value) // 1

shallowRef:
ref()
的浅层作用形式。和 ref() 不同,浅层 ref 的内部值将会原样存储和暴露,并且不会被深层递归地转为响应式。只有对 .value 的访问是响应式的。

const state = shallowRef({ count: 1 })

// 不会触发更改
state.value.count = 2

// 会触发更改
state.value = { count: 2 }

源码实现

  • @issue1 如果是对象和数组,则调用 reactive方法 转化为响应式对象(ref会转换,shallowRef不会转换)
  • @issue2 getter 取值的时候收集依赖
  • @issue3 setter 设置值的时候触发依赖
/**
 * @desc 如果是对象和数组,则转化为响应式对象
 */
function toReactive(value) {
  return isObject(value) ? reactive(value) : value
}

/**
 * @desc RefImpl
 * @issue1 如果是对象和数组,则转化为响应式对象
 */
class RefImpl {
  // ref标识
  public __v_isRef = true
  // 存储effect
  public dep = new Set()
  public _value
  constructor(public rawValue, public _shallow) {
    // @issue1
    this._value = _shallow ? rawValue : toReactive(rawValue)
  }
  get value() {
    // 取值的时候收集依赖
    trackEffects(this.dep)
    return this._value
  }
  set value(newValue) {
    // 新旧值不相等
    if (newValue !== this.rawValue) {
      // @issue1
      this._value = this._shallow ? newValue : toReactive(newValue)
      this.rawValue = newValue
      // 设置值的时候触发依赖
      triggerEffects(this.dep)
    }
  }
}


// 接受一个内部值,返回一个响应式的、可更改的 ref 对象,此对象只有一个指向其内部值的属性 .value。
export function ref(value) {
  return new RefImpl(value)
}

测试代码

/**
 * 1. Ref
 **/
const person = ref({
  name: '柏成',
  age: 25,
})
effect(() => {
  app.innerHTML = person.value.name
})

setTimeout(() => {
  person.value.name = '柏成2号' // 会触发更改
}, 1000)

/**
 * 2. shallowRef
 */
const person = shallowRef({
  name: '柏成',
  age: 25,
})
effect(() => {
  app.innerHTML = person.value.name
})

setTimeout(() => {
  person.value.name = '柏成2号' // 不会触发更改
}, 1000)

setTimeout(() => {
  person.value = {
    name: '柏成9号' // 会触发更改
  }
}, 2000)

toRef & toRefs

toRef:
基于响应式对象上的一个属性,创建一个对应的 ref。这样创建的 ref 与其源属性保持同步:改变源属性的值将更新 ref 的值,反之亦然。

const state = reactive({
  foo: 1,
  bar: 2
})

const fooRef = toRef(state, 'foo')

// 更改该 ref 会更新源属性
fooRef.value++
console.log(state.foo) // 2

// 更改源属性也会更新该 ref
state.foo++
console.log(fooRef.value) // 3

toRefs:
将一个响应式对象转换为一个普通对象,这个普通对象的每个属性都是指向源对象相应属性的 ref。每个单独的 ref 都是使用
toRef()
创建的。

const state = reactive({
  foo: 1,
  bar: 2
})

const stateAsRefs = toRefs(state)

// 这个 ref 和源属性已经 “链接上了”
state.foo++
console.log(stateAsRefs.foo.value) // 2

stateAsRefs.foo.value++
console.log(state.foo) // 3

源码实现

class ObjectRefImpl {
  // 只是将.value属性代理到原始类型上
  constructor(public object, public key) {}
  
  get value() {
    return this.object[this.key]
  }
  
  set value(newValue) {
    this.object[this.key] = newValue
  }
}

// 基于响应式对象上的一个属性,创建一个对应的 ref。这样创建的 ref 与其源属性保持同步:改变源属性的值将更新 ref 的值,反之亦然。
export function toRef(object, key) {
  return new ObjectRefImpl(object, key)
}

// 将一个响应式对象转换为一个普通对象,这个普通对象的每个属性都是指向源对象相应属性的 ref。每个单独的 ref 都是使用 toRef() 创建的。
export function toRefs(object) {
  const result = isArray(object) ? new Array(object.length) : {}

  for (let key in object) {
    result[key] = toRef(object, key)
  }

  return result
}

测试代码

// 对象
const person = reactive({
  name: '柏成',
  age: 18
})
// 数组
const numbers = reactive([1, 2, 3, 4, 5])

// 注意!直接解构后会丢失响应性的特点!!!
// let { name, age } = person

const {
  name,
  age
} = toRefs(person)
const [first] = toRefs(numbers)

effect(() => {
  app.innerHTML = `${name.value},${age.value}岁。第一个数字为${first.value}。`
})

setTimeout(() => {
  name.value = '柏成9号'
  first.value = 999
}, 1000)

自动脱ref

在js中访问ref时需要.value获取,但是在模版中却可以直接取值,不需要加.value!这里就用到了 proxyRefs 自动脱ref方法

源码实现

export function proxyRefs(object) {
  return new Proxy(object, {
    // 代理的思想,如果是ref 则取ref.value
    get(target, key, recevier) {
      let r = Reflect.get(target, key, recevier)
      return r.__v_isRef ? r.value : r
    },
    // 设置的时候如果是ref,则给ref.value赋值
    set(target, key, value, recevier) {
      let oldValue = target[key]
      if (oldValue.__v_isRef) {
        oldValue.value = value
        return true
      } else {
        return Reflect.set(target, key, value, recevier)
      }
    },
  })
}

测试代码

const name = ref('柏成')
const age = ref('24')

const person = proxyRefs({
  name,
  age,
  sex: '男'
})

effect(() => {
  app.innerHTML = `${person.name},${person.age}岁。性别${person.sex}。`
})

setTimeout(() => {
  name.value = '柏成9号'
}, 1000)

本文分享自华为云社区《
中间件是开箱即用的吗?为什么要开发中间件adapter?
》,作者:张俭。

中间件在很多系统中都存在

在一个系统里面,或多或少地都会有中间件的存在,总会有数据库,其他的如消息队列,缓存,大数据组件。即使是基于公有云构筑的系统,公有云厂商只提供广泛使用的中间件,假如你的系统里面有很多组件没那么泛用,那么就只能自己维护,如ZooKeeper、Etcd、Pulsar、Prometheus、Lvs等

什么是中间件adapter

中间件adapter指的是和中间件运行在一起(同一个物理机或同一个容器),使得中间件和商用系统中已有的组件进行对接,最终使得该中间件达到在该系统商用的标准。像Prometheus的众多exporter,就是将中间件和已有的监控系统(Prometheus)进行对接的adpater。

为什么不修改中间件源码直接集成

原因可以有很多,这里我列出几点

源码修改容易,维护困难

很多时候不是社区通用需求,无法合并到社区主干。后续每次中间件版本升级,源码的修改就要重新进行一次。社区大版本代码重构,有的甚至不知道如何修改下去。并且对研发人员的技能要求高。

源码与团队技术栈不同,修改困难

这是最常见的,像java团队维护erlang写的rabbitmq

和其他系统对接,有语言要求

XX监控系统,只能使用X语言接入,但中间件使用Y语言写的,怎么办?adapter的能力就体现出来了。

为什么在商用系统中中间件做不到开箱即用

在商用系统中,对一个新引入的中间件,往往有如下能力上的诉求,原生的中间件很难满足

  • 适配原有的监控系统
  • 适配原有的告警系统
  • 适配原有的证书系统
  • 适配原有的备份系统(如果该中间件有状态)
  • 适配原有的容灾系统(如果该中间件有状态)
  • 自动化能力(适配部署、账号创建、权限策略创建)
  • 对外暴露时封装一层接口
  • 应用程序和中间件的服务发现

有时候,业务也会根据业务的需求对中间件做一些能力增强,这部分需求比较定制,这里无法展开讨论了。

我们来逐一讨论上面列出的能力诉求,凡是adapter能实现的功能,对中间件做修改也能实现,只不过因为上一节列出的原因,选择不在中间件处侵入式修改。

适配原有的监控系统

监控系统获取数据,往往是推拉两种模式,如果该中间件原生不支持和该监控系统对接。我们就可以让adapter先从中间件处取得监控数据,再和监控系统对接

适配原有的告警系统

如果中间件发生了不可恢复的错误,如写事务文件失败,操作ZooKeeper元数据失败,可以通过adapter来识别中间件是否发生了上述不可恢复的错误,并和告警系统对接,发出告警。

适配原有的证书系统

这一点也很关键,开源的中间件,根据我的了解,几乎没有项目做了动态证书轮换的方案,证书基本都不支持变更。而出色的商用系统是一定要支持证书轮换的。不过很遗憾的是,这些涉及到TLS握手的关键流程,adapter无法干涉这个流程,只能对中间件进行侵入式修改。

适配原有的备份系统

通过adapter对中间件进行定期备份、按照配置中心的策略备份、备份文件自动上传到文件服务器等。

适配原有的容灾系统

这个视中间件而定,有些中间件如Pulsar原生支持跨地域容灾的话,我们可能做一做配置就好了。另外一些,像mysql和mongo这种,可能我们还需要通过adapter来进行数据同步。不过这个时候adapter负责的职责就大了,还包括了容灾能力。

自动化能力

自动化部署

比如ZooKeeper、Kafka、filebeat在安装的时候,要求填写配置文件,我们就可以让adapter来自动化生成配置或更新配置

账号和策略的创建更新

像kubernetes、mysql、mongo,我们可以在安装的时候通过adapter来自动化创建或更新

对外暴露时封装一层接口

封装接口常用于中间件的提供者,出于种种原因,如中间件原本接口能力太大、中间件原本接口未做权限控制、中间件原本接口未适配期望的权限框架等。我们可以用adapter封装实现一层新的接口对外暴露。

应用程序和中间件的服务发现

应用程序发现中间件

应用程序与中间件的连接,说的简单一点就是如何获取Ip,如果是基于kubernetes的部署,那么不推荐配置Ip,最好是配置域名,因为Ip会跟着容器的生命周期变化。首先,你的应用程序并不会因为中间件的一个容器重启了来重建客户端,往往是通过一个简单重连的方式连接到新的中间件容器继续工作。其次,我们的运维人员也不会每时每刻盯着容器Ip是否变化来进行配置吧。以下图为例,域名的配置要优于Ip的配置。

cke_150.png

截止到目前,我们只需要一个静态配置,使得应用程序可以连接到中间件。最好这个配置是可以修改的,这样我们还可以继承蓝绿、灰度发布的能力。

中间件到业务程序的发现

这个模式常用于负载均衡中间件如Lvs、Nginx自动维护后端列表,我们可以通过adapter来从注册中心获取后端服务的实例信息,并实时更新。

总结

在商用系统中,中间件并没有想象中的那么开箱即用,本文讲述了一些中间件集成到商用系统中需要具备的能力。在对中间件侵入式修改没有技术能力或不想对中间件进行侵入式修改的场景。选用团队常用的、占用资源少的语言来开发中间件adapter应该是更好的选择。

点击关注,第一时间了解华为云新鲜技术~

前言

OpenCV是一个基于Apache2.0许可(开源)发行的跨平台计算机视觉和机器学习软件库,它具有C++,Python,Java和MATLAB接口,并支持Windows,Linux,Android和Mac OS。 Emgu CV是OpenCV图像处理库的跨平台 .Net 包装器。允许从 .NET 兼容语言调用OpenCV函数。但是网上目前关于在Mac OS上使用EmguCV的教程较少,而我后续推出的OpenVINO C# API项目将支持Mac OS系统,为了大家后续能够使用,特出一期教程来演示一下Mac OS上使用EmguCV。

1. 项目环境

  • 编码环境:Visual Studio Code
  • 程序框架:.NET 6.0

目前在Mac OS上使用C#语言官方提供了编译
Visual Studio for Mac
,但是根据官方发布的通知后续将不再支持该软件更新,后续将全部转移到
Visual Studio Code
平台,所以在此处我们演示使用
Visual Studio Code
进行演示。而代码的运行与配置使用
dotnet
指令实现。

关于
Visual Studio Code
以及
.NET
的安装方式可以参考一下官方教程:
在 macOS 上安装 .NET

Visual Studio Code on macOS

2. 创建控制台项目

此处使用
dotnet
指令创建新项目,在
Visual Studio Code
的终端中输入一下指令:

dotnet new console --framework net6.0 --use-program-main -o test_emgucv

如下图所示,在终端中输入以下指令后,会自动创建新的项目以及项目文件夹。

image

在创建好项目后,我们进行一下项目测试,依次输入以下指令,最后输出如下图所示:

cd test_emgucv
dotnet run

image

3. 添加 Nuget Package 程序包

Emgu CV是一个可以跨平台使用的程序包,并且官方也提供了编译好的程序包,用户可以根据自己的平台进行安装。在Mac OS上,主要需要安装一下两个包,分别是Emgu.CV的官方程序包以及Emgu.CV的运行依赖包。

dotnet add package Emgu.CV
dotnet add package Emgu.CV.runtime.mini.macos

安装完上面两个安装包后,项目的配置的文件中会增加下面两个配置。

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net6.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Emgu.CV" Version="4.8.1.5350" />
    <PackageReference Include="Emgu.CV.runtime.mini.macos" Version="4.8.1.5350" />
  </ItemGroup>

</Project>

接下来运行
dotnet run
,检验项目中是否包含所需要的配置文件:
Emgu.CV.dll

runtimes/osx/native/libcvextern.dylib
。打开项目运行生成的文件夹
bin/{build_config}/{dotnet_version}/
,在本项目中是
bin/Debug/net6.0/
文件夹,如下图所示:

image

通过该图可以看出,在本项目中只有
Emgu.CV.dll
文件,并没有
runtimes/osx/native/libcvextern.dylib
文件,因该文件需要我们自行配置。首先是需要找到该文件,该文件主要是在
Emgu.CV.runtime.mini.macos
程序包中,如下图所示:

image

接下来就是创建
runtimes/osx/native/
文件夹,然后将该文件放在该文件夹下即可。如下图所示:

image

3. 测试应用

最后我们编写项目代码进行测试,如下面代码所示:

using System;
using Emgu.CV;
using Emgu.Util;
using Emgu.CV.Structure;
using Emgu.CV.CvEnum;
namespace test_emgucv 
{
    internal class Program
    {
        static void Main(string[] args)
        {
            Mat image = CvInvoke.Imread("image.jpg");
            Mat image2=new Mat();
            if (!image.IsEmpty)
            {
                Console.WriteLine("srcImg is OK!");
            }
            Console.WriteLine("图像的宽度是:{0}",image.Rows);
            Console.WriteLine("图像的高度是:{0}", image.Cols);
            Console.WriteLine("图像的通道数是:{0}", image.NumberOfChannels);
            CvInvoke.Imshow("src", image);
            CvInvoke.CvtColor(image, image2, ColorConversion.Bgr2Gray);//转为灰度图像
            CvInvoke.Imshow("src1", image2);
            CvInvoke.WaitKey(0);
            CvInvoke.DestroyAllWindows();//销毁所有窗口
        }
    }
}

项目代码运行后,最后呈现效果如下图所示:

image

4. 总结

在本次项目中,我们成功实现了在Mac OS上使用EmguCV,并成功配置了EmguCV依赖库,实现了在.NET 6.0环境下使用C#语言调用EmguCV库,实现的图片数据的读取以及图像色彩转换,并进行了图像展示。

C++ 动态库热加载

本文参考自
project-based-learning
中的
Build a Live Code-reloader Library for C++
,主要内容都来自于其中,但是对代码进行了一点修改,并且改用 CMake 进行构建。

文章整体比较基础,适合初学者,通过本文可以学习到以下知识点

  1. 关于 C++ 程序如何编译运行,如何运行时加载动态库(使用
    dl*
    API)。
  2. 如何设计简洁易用的库 API 供用户使用。
  3. 如何使用 CMake 组织并构建一个包含可执行程序、动态库和头文件库的项目。
  4. 如何使用 GoogleTest 进行测试。

动态库热加载原理

动态库热加载指的是在程序运行时,动态地加载动态库,从而达到不停止程序的情况下,更新程序的功能。

C++ 程序在运行时有两种方式加载动态连接库:隐式链接和显式链接
[1]

  1. 隐式链接就是在编译的时候使用
    -l
    参数链接的动态库,进程在开始执行时就将动态库文件映射到内存空间中。
  2. 显式链接使用
    libdl.so
    库的 API 接口在运行中加载和卸载动态库,主要的 API 有
    dlopen、dlclose、dlsym、dlerror

隐式链接的方式要进行热加载需要不少 Hack,难度较大,本文主要讲解第二种方式。

简单版本

首先我们快速实现一个能够完成最小功能可运行的版本,熟悉相关 API 的使用。我们简单编写三个文件,分别为
main.cpp

replex.h

hello.cpp
,另外还编写一个快速编译运行代码的脚本
run.sh
,目录结构如下

.
├── hello.cpp
├── main.cpp
├── replex.h
└── run.sh

代码的完整版本见
projects/replex-1

replex.h
中对
dl*
API 进行了简单的封装,使用一个 namespace 将 API 进行了包装,代码如下

#pragma once

#include <dlfcn.h>

#include <cstdio>

namespace Replex {

inline void* Load(const char* filepath) {
    return dlopen(filepath, RTLD_LAZY);
}

inline void* LoadSymbol(void* library, const char* symbol) {
    return dlsym(library, symbol);
}

inline void Reload(void*& library, const char* filepath) {
    if (library) {
        dlclose(library);
    }
    library = Load(filepath);
}

inline void PrintError() {
    fprintf(stderr, "%s\n", dlerror());
}

}  // namespace Replex

hello.cpp
是我们需要热加载的动态库,代码如下

#include <cstdio>

extern "C" {
void foo() {
    printf("Hi\n");
}

int bar = 200;
}

其中使用
extern "C"

foo

bar
声明为 C 语言的函数和变量,这样在编译时就不会对函数名进行修饰,否则在
main.cpp
中使用
dlsym
时会找不到
foo
对应的符号。

不加
extern "C"
时,使用
nm
命令查看
hello.so
中的符号如下

$ nm libhello.so  | grep foo
0000000000006666669 T _Z3foov

加上后

$ nm libhello.so  | grep foo
0000000000006666669 T foo

main.cpp
是主程序,代码如下

#include <cstdio>
#include <string>

#include "replex.h"

const char* g_libPath = "libhello.so";

int main() {
    void* handle;
    void (*foo)();
    int bar;

    handle = Replex::Load(g_libPath);
    if (!handle) {
        Replex::PrintError();
        return -1;
    }
    foo = reinterpret_cast<void (*)()>(Replex::LoadSymbol(handle, "foo"));
    foo();
    bar = *reinterpret_cast<int*>(Replex::LoadSymbol(handle, "bar"));
    printf("bar == %d\n", bar);

    // Modify the source code and recompile the library.
    std::string filename = "hello.cpp";
    std::string command = std::string("sed -i ") +
                          (bar == 200 ? "'s/200/300/'" : "'s/300/200/'") + " " +
                          filename;
    system(command.c_str());
    command = std::string("sed -i ") +
              (bar == 200 ? "'s/Hi/Hello/'" : "'s/Hello/Hi/'") + " " + filename;
    system(command.c_str());
    system("g++ -shared -fPIC -o libhello.so hello.cpp");

    Replex::Reload(handle, g_libPath);
    if (!handle) {
        Replex::PrintError();
        return -1;
    }
    foo = reinterpret_cast<void (*)()>(Replex::LoadSymbol(handle, "foo"));
    foo();
    bar = *reinterpret_cast<int*>(Replex::LoadSymbol(handle, "bar"));
    printf("bar == %d\n", bar);

    return 0;
}

整体代码逻辑比较好懂,首先加载动态库,然后获取动态库中的函数和变量,调用函数和打印变量,然后修改
hello.cpp
中的代码,重新编译动态库,再次加载动态库,调用函数和打印变量。

reinterpret_cast
是 C++ 中的强制类型转换,将
void*
指针转换为函数指针和变量指针。

run.sh
的内容如下

#!/bin/bash
set -e # stop the script on errors
g++ -fPIC -shared -o libhello.so hello.cpp
g++ -o main.out main.cpp -ldl
./main.out

脚本中
-shared -fPIC
参数用于生成位置无关的动态库,
-ldl
参数用于链接
libdl.so
库(
dl*
API),
-o
参数用于指定输出文件名。

运行脚本后,输出如下

Hi
bar == 200
Hello
bar == 300

当前程序能够完成基本功能,但是对于使用者来说我们的库不够好用,使用者(
main.cpp
)需要自己定义相应的函数指针和类型,还需要自己进行类型转换,动态库的导出符号也需要自己定义,对于使用者来说也相当麻烦。

改进版本

我们考虑提供更简单的接口供用户使用,我们将在
replex.h
中创建一个
ReplexModule
类,这个类将用于给动态库的继承使用,然后由动态库的作者提供更加简明的接口供用户使用。

这一版本代码的完整实现见
GitHub

最终的使用效果见如下
main.cpp
文件

#include <iostream>

#include "hello.h"

int main() {
    HelloModule::LoadLibrary();
    HelloModule::Foo();
    int bar = HelloModule::GetBar();
    std::cout << "bar == " << bar << std::endl;

    // Modify the source code and recompile the library.
    // ...

    HelloModule::ReloadLibrary();
    HelloModule::Foo();
    std::cout << "bar == " << HelloModule::GetBar() << std::endl;
    return 0;
}

我们忽略中间的修改源码和重新编译的过程,这里只关注
HelloModule
的使用,相比于前一版本,这里的使用更加简单,不需要自己定义函数指针和变量,也不需要自己进行类型转换,只需要调用
HelloModule
中的接口即可。同时注意到我们包含的头文件也变成了
hello.h
,这个头文件是动态库作者提供的,我们在
main.cpp
中只需要包含这个头文件即可。

针对于上述需求,
ReplexModule
需要公开两个公共接口,一个用于发布可热加载库,另一个用于加载和重新加载这些可热加载库。

ReplexModule
的公开接口仅有两个,分别为
LoadLibrary

ReloadLibrary
,代码如下

#pragma once

#include <dlfcn.h>

#include <array>
#include <iostream>
#include <stdexcept>
#include <string>
#include <unordered_map>

template <typename E, size_t NumSymbols>
class ReplexModule {
   public:
    static void LoadLibrary() { GetInstance().Load(); }
    static void ReloadLibrary() { GetInstance().Reload(); }

   protected:
    static E& GetInstance() {
        static E instance;
        return instance;
    }

    // ...
    // ... continued later
}

这两个函数都依赖于
GetInstance
函数,这个函数是一个模板函数,用于返回
ReplexModule
的子类的单例,这样可以保证每个子类只有一个实例。另外,
ReplexModule
是一个模板类,模板参数
E
是一个枚举类型,用于指定动态库中的符号,
NumSymbols
是一个常量,用于指定动态库中的符号个数。

接下来关注
ReplexModule
向动态库作者也就是集成该类的子类提供的接口,代码如下:

    // ... continued above

    // Should return the path to the library on disk
    virtual const char* GetPath() const = 0;

    // Should return a reference to an array of C-strings of size NumSymbols
    // Used when loading or reloading the library to lookup the address of
    // all exported symbols
    virtual std::array<const char*, NumSymbols>& GetSymbolNames() const = 0;

    template <typename Ret, typename... Args>
    Ret Execute(const char* name, Args... args) {
        // Lookup the function address
        auto symbol = m_symbols.find(name);
        if (symbol != m_symbols.end()) {
            // Cast the address to the appropriate function type and call it,
            // forwarding all arguments
            return reinterpret_cast<Ret (*)(Args...)>(symbol->second)(args...);
        }
        throw std::runtime_error(std::string("Function not found: ") + name);
    }

    template <typename T>
    T* GetVar(const char* name) {
        auto symbol = m_symbols.find(name);
        if (symbol != m_symbols.end()) {
            return static_cast<T*>(symbol->second);
        }
        // We didn't find the variable. Return an empty pointer
        return nullptr;
    }

   private:
    void Load() {
        m_libHandle = dlopen(GetPath(), RTLD_NOW);
        LoadSymbols();
    }

    void Reload() {
        auto ret = dlclose(m_libHandle);
        m_symbols.clear();
        Load();
    }

    void LoadSymbols() {
        for (const char* symbol : GetSymbolNames()) {
            auto* sym = dlsym(m_libHandle, symbol);
            m_symbols[symbol] = sym;
        }
    }

    void* m_libHandle;
    std::unordered_map<std::string, void*> m_symbols;
};

首先关注最底部的数据成员,
m_libHandle
是动态库的句柄,
m_symbols
是一个哈希表,用于存储动态库中的符号和符号对应的地址。
Load
函数用于加载动态库,
Reload
函数用于重新加载动态库,
LoadSymbols
函数用于加载动态库中的符号,这几个函数的逻辑相当清晰无需赘述。

值得讲解的是
Execute

GetVar
函数,
Execute
函数用于调用动态库中的函数,
GetVar
函数用于获取动态库中的变量,让我们先看看
Execute
函数的实现,代码如下

    template <typename Ret, typename... Args>
    Ret Execute(const char* name, Args... args) {
        // Lookup the function address
        auto symbol = m_symbols.find(name);
        if (symbol != m_symbols.end()) {
            // Cast the address to the appropriate function type and call it,
            // forwarding all arguments
            return reinterpret_cast<Ret (*)(Args...)>(symbol->second)(args...);
        }
        throw std::runtime_error(std::string("Function not found: ") + name);
    }

这是一个模板函数,模板参数
Ret
是返回值类型,
Args...
是参数类型,这里的
Args...
表示可以接受任意多个参数,
Args... args
表示将参数包
args
展开,然后将展开后的参数作为参数传递给
Execute
函数。

该函数首先在
m_symbols
中查找
name
对应的符号,如果找到了,就将符号地址转换为类型为
Ret (*)(Args...)
的函数指针,然后调用该函数,传递参数
args...
,如果没有找到,就抛出异常。

GetVar
函数的实现如下

    template <typename T>
    T* GetVar(const char* name) {
        auto symbol = m_symbols.find(name);
        if (symbol != m_symbols.end()) {
            return static_cast<T*>(symbol->second);
        }
        // We didn't find the variable. Return an empty pointer
        return nullptr;
    }

该函数的实现和
Execute
函数类似,只是将函数指针转换为变量指针,然后返回。

hello.cpp
的内容保持不变:

#include <cstdio>

extern "C" {
void foo() {
    printf("Hi\n");
}

int bar = 200;
}

hello.h
中定义类
HelloModule
继承自
ReplexModule
,代码如下

#pragma once
#include <array>

#include "replex.h"

inline std::array<const char*, 2> g_exports = {"foo", "bar"};

class HelloModule : public ReplexModule<HelloModule, g_exports.size()> {
   public:
    static void Foo() { GetInstance().Execute<void>("foo"); }

    static int GetBar() { return *GetInstance().GetVar<int>("bar"); }

   protected:
    virtual const char* GetPath() const override { return "libhello.so"; }

    virtual std::array<const char*, g_exports.size()>& GetSymbolNames()
        const override {
        return g_exports;
    }
};

变量
g_exports
用于存储动态库中需要导出的符号,其采用
inline
修饰,这样就可以在头文件中定义,而不会出现重复定义的错误。

HelloModule
中定义了两个静态函数,分别为
Foo

GetBar
,这两个函数用于调用动态库中的函数和获取动态库中的变量。

运行脚本的内容基本不变,添加了
-std=c++17
的标志保证可以使用
inline
变量的用法。

#!/bin/bash
set -e # stop the script on errors
g++ -fPIC -shared -o libhello.so hello.cpp -std=c++17
g++ -o main.out main.cpp -ldl -std=c++17
./main.out

运行效果与前一版本一致,如下

Hi
bar == 200
Hello
bar == 300

现在我们可以认为我们所编写的
replex.h
库足方便使用,动态库作者只需要继承
ReplexModule
类,然后实现两个虚函数即可,使用者只需要包含动态库作者提供的头文件,然后调用相应的接口即可。

CMake 版本

前面两个版本的代码都是写个脚本直接使用
g++
编译,这样的方式不够灵活,不利于项目的管理,正好这个项目涉及到几个不同的模块,可以尝试使用
CMake
进行管理,学习一下项目的组织构建。

完整代码见
projects/replex-3
,采用
现代 CMake 模块化项目管理指南
中推荐的方式进行项目组织,但是略微进行了一点简化,目录结构如下

.
├── CMakeLists.txt
├── hello
│   ├── CMakeLists.txt
│   ├── include
│   │   └── hello.h
│   └── src
│       └── hello.cpp
├── main
│   ├── CMakeLists.txt
│   └── src
│       └── main.cpp
└── replex
    ├── CMakeLists.txt
    └── include
        └── replex.h

首先梳理一下整个项目的依赖关系,如下所示

main (exe)
├── hello_interface (interface)
│   └── replex (interface)
└── hello (shared lib)

main 模块依赖于头文件库 hello_interface,hello_interface 依赖于头文件库 replex,动态库 hello 不依赖于任何库,用于提供给 main 模块使用。

CMakeLists.txt
为根目录的
CMakeLists.txt
,内容如下

cmake_minimum_required(VERSION 3.15)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

project(replex LANGUAGES CXX)

if (NOT CMAKE_BUILD_TYPE)
    set(CMAKE_BUILD_TYPE Release)
endif ()

add_subdirectory(replex)
add_subdirectory(main)
add_subdirectory(hello)

首先设置 C++ 标准,然后设置项目名称,然后判断是否设置了构建类型,如果没有设置,则设置为 Release 模式,然后添加子目录,分别为 replex、main 和 hello。

replex/CMakeLists.txt
的内容如下

add_library(replex INTERFACE include/replex.h)
target_include_directories(replex INTERFACE include)

replex 为头文件库,使用
add_library
添加,类型为 INTERFACE,表示这是一个接口库,不会生成任何文件,只会导出头文件,使用
target_include_directories
添加头文件路径。

hello/CMakeLists.txt
的内容如下

add_library(hello SHARED src/hello.cpp)

add_library(hello_interface INTERFACE include/hello.h)
target_include_directories(hello_interface INTERFACE include)
target_link_libraries(hello_interface INTERFACE replex)

其中定义了两个库,一个为动态库 hello,一个为头文件库 hello_interface 用于导出 动态库 hello 中的符号以供使用, hello_interface 依赖于 replex,使用
target_link_libraries
添加依赖。

main/CMakeLists.txt
的内容如下

add_executable(main src/main.cpp)
target_link_libraries(main PRIVATE hello_interface)

main 为可执行文件,使用
add_executable
添加,使用
target_link_libraries
添加依赖
hello_interface

最后运行脚本
run.sh
,内容如下

#!/bin/bash
set -e # stop the script on errors
cmake -B build
cmake --build build
./build/main/main

运行的效果如下

Hi
bar == 200
[  0%] Built target replex
[  0%] Built target hello_interface
[ 50%] Built target main
[ 75%] Building CXX object hello/CMakeFiles/hello.dir/src/hello.cpp.o
[100%] Linking CXX shared library libhello.so
[100%] Built target hello
Hello
bar == 300

添加测试 (
GoogleTest

这部分的完整代码见
projects/replex-4

一个好的项目,测试是必不可少的,前面我们实现的
main.cpp
中其实已经有了一点自动化测试的影子,但是这种方式不够好,我们可以使用 GoogleTest 来进行测试。

首先演示一个最基本的 gtest 用法,首先使用 git 的
submodule
命令添加 googletest 到我们的项目中

git submodule add git@github.com:google/googletest.git

然后修改我们根目录下的 CMakeLists.txt,添加如下内容

add_subdirectory(googletest)
enable_testing()
include_directories(${gtest_SOURCE_DIR}/include ${gtest_SOURCE_DIR})

add_subdirectory(test)

创建 test 目录,结构如下

test
├── CMakeLists.txt
└── src
    └── test.cpp

test/CMakeLists.txt
的内容如下

add_executable(tests src/test.cpp)
target_link_libraries(tests PUBLIC gtest gtest_main)

test/src/test.cpp
的内容如下

#include <gtest/gtest.h>

TEST(SillyTest, IsFourPositive) {
    EXPECT_GT(4, 0);
}

TEST(SillyTest, IsFourTimesFourSixteen) {
    int x = 4;
    EXPECT_EQ(x * x, 16);
}

int main(int argc, char** argv) {
    // This allows us to call this executable with various command line
    // arguments which get parsed in InitGoogleTest
    ::testing::InitGoogleTest(&argc, argv);
    return RUN_ALL_TESTS();
}

OK,到现在我们已经成功添加了 GoogleTest 到我们的项目中并且可以运行测试了,现在我们要编写一些测试来测试我们的项目。

我们编写一个 replex 的测试,测试内容如下

#include <gtest/gtest.h>
#include <hello.h>

#include <cstdlib>
#include <fstream>

const char* g_Test_v1 = R"delimiter(
extern "C" {
int foo(int x) {
    return x + 5;
}
int bar = 3;
}
)delimiter";

const char* g_Test_v2 = R"delimiter(
extern "C" {
int foo(int x) {
    return x - 5;
}
int bar = -2;
}
)delimiter";

class ReplexTest : public ::testing::Test {
   public:
    // Called automatically at the start of each test case.
    virtual void SetUp() {
        WriteFile("hello/src/hello.cpp", g_Test_v1);
        Compile(1);
        HelloModule::LoadLibrary();
    }

    // We'll invoke this function manually in the middle of each test case
    void ChangeAndReload() {
        WriteFile("hello/src/hello.cpp", g_Test_v2);
        Compile(2);
        HelloModule::ReloadLibrary();
    }

    // Called automatically at the end of each test case.
    virtual void TearDown() {
        HelloModule::UnloadLibrary();
        WriteFile("hello/src/hello.cpp", g_Test_v1);
        Compile(1);
    }

   private:
    void WriteFile(const char* path, const char* text) {
        // Open an output filetream, deleting existing contents
        std::ofstream out(path, std::ios_base::trunc | std::ios_base::out);
        out << text;
    }

    void Compile(int version) {
        if (version == m_version) {
            return;
        }

        m_version = version;
        EXPECT_EQ(std::system("cmake --build build"), 0);

        // Super unfortunate sleep due to the result of cmake not being fully
        // flushed by the time the command returns (there are more elegant ways
        // to solve this)
        sleep(1);
    }

    int m_version = 1;
};

TEST_F(ReplexTest, VariableReload) {
    EXPECT_EQ(HelloModule::GetBar(), 3);
    ChangeAndReload();
    EXPECT_EQ(HelloModule::GetBar(), -2);
}

TEST_F(ReplexTest, FunctionReload) {
    EXPECT_EQ(HelloModule::Foo(4), 9);
    ChangeAndReload();
    EXPECT_EQ(HelloModule::Foo(4), -1);
}

int main(int argc, char** argv) {
    ::testing::InitGoogleTest(&argc, argv);
    return RUN_ALL_TESTS();
}

要使得这个测试运行起来,还需要对 CMake 文件进行一些修改,这部分留作练习吧,动手试试会对 CMake 等有更深的理解。

相比较于
projects/replex-3
,需要修改的文件有:

  1. 移除 main 文件夹
  2. 根目录下的 CMakeLists.txt
  3. hello/CMakeLists.txt
  4. hello/include/hello.h
  5. test/src/test.cpp

完整代码见
projects/replex-4


  1. Linux 下 C++so 热更新
    ↩︎

说起模板元编程,估计不少人的第一反应都是充斥着各种递归的奇技淫巧,没错,这次我们就来对模板元这种屠龙之术进行初步窥探,看看能玩出什么花样出来。

小试牛刀

template <typename _Tp, typename... args>
struct get_size {
    static constexpr std::size_t value = get_size<args...>::value + 1;
};

template <typename _Tp>
struct get_size<_Tp> {
    static constexpr std::size_t value = 1;
};

get_size用于获取参数类型列表的类型个数(可以包括重复的类型),因为要用到递归实现,所以很容易推出f(n) = f(n - 1) + 1的结构。不过递归要写终结条件,这里假定最少的参数个数为1,只需要对模板参数类型为1的情况进行特化。如果有一定的基础,上面这段代码是不难理解的。

再来一个

template <bool __Cond, bool... args>
struct all_of {
    static constexpr bool value = __Cond && all_of<args...>::value;
};

template <bool __Cond>
struct all_of<__Cond> {
    static constexpr bool value = __Cond;
};

这个模板可用于判断参数列表的所有bool值是否都为真,若为真则true,反之false;其实和上面的也差不多,没有用到多复杂的递归。

同理,any_of也是类似的。

template <bool __Cond, bool... args>
struct any_of {
    static constexpr bool value = __Cond || any_of<args...>::value;
};

template <bool __Cond>
struct any_of<__Cond> {
    static constexpr bool value = __Cond;
};

上点强度

template <std::size_t __first, std::size_t __second, std::size_t... args>
struct is_ascending {
    static constexpr bool value = (__first <= __second) && is_ascending<__second, args...>::value;
};

template <std::size_t __first, std::size_t __second>
struct is_ascending<__first, __second> {
    static constexpr bool value = (__first <= __second);
};

判断模板参数序列是否是递增的(非严格递增),类似地,还有

template <std::size_t __first, std::size_t __second, std::size_t... args>
struct is_strictly_ascending {
    static constexpr bool value = (__first < __second) &&
    is_strictly_ascending<__second, args...>::value;
};

template <std::size_t __first, std::size_t __second>
struct is_strictly_ascending<__first, __second> {
    static constexpr bool value = (__first < __second);
};


template <std::size_t __first, std::size_t __second, std::size_t... args>
struct is_descending {
    static constexpr bool value = (__first >= __second) && is_descending<__second, args...>::value;
};

template <std::size_t __first, std::size_t __second>
struct is_descending<__first, __second> {
    static constexpr bool value = (__first >= __second);
};


template <std::size_t __first, std::size_t __second, std::size_t... args>
struct is_strictly_descending {
    static constexpr bool value = (__first > __second) && 
                                      is_strictly_descending<__second, args...>::value;
};

template <std::size_t __first, std::size_t __second>
struct is_strictly_descending<__first, __second> {
    static constexpr bool value = (__first > __second);
};

进阶

std::tuple和std::variant都是C++当中接受可变模板参数的容器。试想一下如何实现这种操作:std::tuple<int, double>, std::tuple<char, float>,合并之后变成了std::tuple<int, double, char, float>。

template <typename... args1, typename... args2>
struct tuple_type_cat
{};

如果出现了诸如此类的声明,会发现编译器报错,原因是可变参数必须在后面。那难道就没有办法了吗,其实还是有的,得“套中套”。

template <typename... args>
struct tuple_type_cat;

template <typename... args1, typename... args2>
struct tuple_type_cat<std::tuple<args1...>, std::tuple<args2...>> {
    using type = std::tuple<args1..., args2...>;
};

可以先声明一个可变参数模板的结构体,然后到具体实现的时候,这样就可以使用两个可变模板参数了。可以简单理解为,一个可变模板参数可以展开成两个可变模板参数。这样就可以避开声明的时候使用两个可变模板参数从而出现报错的情况。

std::variant也一样,来编写测试看看。

// variant_type_cat
template <typename... args>
struct variant_type_cat;

template <typename... args1, typename... args2>
struct variant_type_cat<std::variant<args1...>, std::variant<args2...>> {
    using type = std::variant<args1..., args2...>;       
};

int main()
{
    using v1 = std::variant<int, double>;
    using v2 = std::variant<char, float>;
    using v3 = variant_type_cat<v1, v2>::type;
    using tmp = std::variant<int, double, char, float>;

    static_assert(std::is_same_v<v3, tmp>);
}

下次有时间再深剖更多有关模板元的技术。