macOS下编译C++文件

WebAssembly 是一种新的编译目标,可以将 C/C++ 等语言编译成 WebAssembly,然后在浏览器中运行。我最近在研究如何把zbar (opens new window)编译成 WebAssembly,然后在浏览器中使用。第一步就是想把官方的例子编译成二进制测试下代码,但是在 MacOS 下编译的时候遇到了一些问题,这里记录下。

# 官方例子

scan_image.cpp (opens new window)

官方提供了一个例子,可以用来测试二维码识别功能,这个例子有两个依赖 Magick++.h 和 zbar.h,这两个库都可以通过 brew 安装。

brew install imagemagick
brew install zbar
1
2

因为从源码编译对我来说实在是太复杂了,所以我就选择了直接安装相关依赖。

# 编译

g++ -o scan_image scan_image.cpp
1

执行上面的命令,会报错:

scan_image.cpp:1:10: fatal error: 'Magick++.h' file not found
1

这是因为 Magick++.h 的头文件在 /opt/homebrew/include/ImageMagick-7/Magick++.h,但是 g++ 默认只会在 /opt/homebrew/include 目录下查找头文件,所以我们需要指定头文件的路径。

g++ -o scan_image scan_image.cpp -I/opt/homebrew/include/ImageMagick-7
1

执行上面的命令还是会有报错:

/opt/homebrew/include/ImageMagick-7/MagickCore/magick-config.h:63:3: warning: "you should set MAGICKCORE_QUANTUM_DEPTH to sensible default set it to configure time default" [-W#warnings]
# warning "you should set MAGICKCORE_QUANTUM_DEPTH to sensible default set it to configure time default"
  ^
/opt/homebrew/include/ImageMagick-7/MagickCore/magick-config.h:64:3: warning: "this is an obsolete behavior please fix your makefile" [-W#warnings]
# warning "this is an obsolete behavior please fix your makefile"
  ^
/opt/homebrew/include/ImageMagick-7/MagickCore/magick-config.h:86:3: error: "you should set MAGICKCORE_HDRI_ENABLE"
# error "you should set MAGICKCORE_HDRI_ENABLE"
  ^
/opt/homebrew/include/ImageMagick-7/MagickCore/magick-config.h:121:3: error: "you should set MAGICKCORE_HDRI_ENABLE"
# error "you should set MAGICKCORE_HDRI_ENABLE"
1
2
3
4
5
6
7
8
9
10
11

我们需要设置两个 ImageMagick 需要的编译变量:

g++ -o scan_image scan_image.cpp -I/opt/homebrew/include/ImageMagick-7 -DMAGICKCORE_QUANTUM_DEPTH=16 -DMAGICKCORE_HDRI_ENABLE=0
1

设置了这两个变量之后,还是会报错:

Undefined symbols for architecture arm64:
  "Magick::Blob::Blob()", referenced from:
      _main in scan_image-727bdc.o
  "_zbar_get_symbol_name", referenced from:
      zbar::Symbol::get_type_name() const in scan_image-727bdc.o
ld: symbol(s) not found for architecture arm64
clang: error: linker command failed with exit code 1 (use -v to see invocation)
1
2
3
4
5
6
7

这是因为我们只引用了头文件,却没有引用相关库的实现。我们需要指定 ImageMagick 的库文件路径,这里我使用的是 brew 安装的 ImageMagick,所以库文件在 /opt/homebrew/lib/ImageMagick 目录下。

g++ -o scan_image scan_image.cpp -I/opt/homebrew/include/ImageMagick-7 -L/opt/homebrew/lib/ImageMagick -DMAGICKCORE_QUANTUM_DEPTH=16 -DMAGICKCORE_HDRI_ENABLE=0 -lMagick++-7.Q16HDRI -lzbar
1

同时我们需要使用 -l 参数来指定需要链接的库,这里需要链接 Magick++-7.Q16HDRI 和 zbar。

# CMake

上面的编译命令比较长,而且如果我们需要编译多个文件的时候,就需要写很多次,所以我们可以使用 CMake 来简化编译过程。

cmake_minimum_required(VERSION 3.10)
project(ScanImage)


set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_FLAGS "-DMAGICKCORE_HDRI_ENABLE=1 -DMAGICKCORE_QUANTUM_DEPTH=16")

include_directories(/opt/homebrew/include/ImageMagick-7)
link_directories(/opt/homebrew/lib/ImageMagick)

add_executable(ScanImage scan_image.cpp)
target_link_libraries(ScanImage zbar Magick++-7.Q16HDRI)
1
2
3
4
5
6
7
8
9
10
11
12

# 总结

C++ 的编译报错总是让人摸不着头脑,这里我只是简单的记录了下我遇到的问题和解决方案,希望能帮助到大家。

# 参考

how to #include third party libraries (opens new window)

zbar-wasm (opens new window)

正则表达式匹配信用卡

最近在做支付相关的服务,需要检测用户输入的信用卡号是否合法,并且根据信用卡号规则显示对应的信用卡类型。这里记录一下如何使用正则表达式来匹配信用卡号。顺便说一下只有 AE 卡号的 CVV 是 4 位的,其他的都是 3 位。

# 银联卡

银联卡以 62 开头,借记卡是 19 位,信用卡是 14 位。一般支付的时候都是使用信用卡,所以我们只需要匹配 14 位的卡号。

const regex = new RegExp('^62\\d{14}');
1

# Visa

Visa 信用卡以 4 开头,新卡长度为 16 位,旧卡长度为 13 位。

const regex = new RegExp('^4[0-9]{12}(?:[0-9]{3})?');
1

# MasterCard

MasterCard 信用卡以 51 到 55 开头或者是以 2221–2720 开头,长度为 16 位。

const regex = new RegExp('^(5[1-5]\\d{0,2}|22[2-9]\\d{1}|2[3-7]\\d{2})\\d{12}');
1

# Amex

Amex 信用卡以 34 或者 37 开头,长度为 15 位。

const regex = new RegExp('^3[47]\\d{13}');
1

# Jcb

Jcb 信用卡以 35 开头,长度为 16 位。以 2131 或者 1800 开头,长度为 15 位。

const regex = new RegExp('^(?:2131|1800|35\\d{3})\\d{11}');
1

# Diners

Diners 信用卡以 36、38 或者 30[0-5] 开头,长度为 14 位。

const regex = new RegExp('^3(?:0([0-5]|9)|[68]\\d)\\d{11}');
1

# 总结

信用卡号的判断并不复杂,但是很多卡号有些特殊情况,需要注意。

# 参考

Payment card number (opens new window)

Finding or Verifying Credit Card Numbers (opens new window)

无限滚动加载列表

滚动加载是分页显示列表常用的一个技术,具体的实现方式基本上后端都会在返回数据上给一个 cursor 字段,前端在请求下一页的时候会把这个字段带上,后端根据这个字段来返回下一页的数据。这种方式的好处是可以在前端缓存数据,减少请求次数,但是缺点也很明显,就是无法跳页,只能一页一页的往下翻。为此,我们要实现一个滚动加载组件,当这个组件在可视范围内的时候,会去加载下一页的数据。

# 核心技术点

如果检测组件进入可视范围,我们可以使用 IntersectionObserver API,来监听元素进入可视范围。我们使用 React 实现一个组件,当这个组件进入可视范围的时候,会触发一个回调函数,这个回调函数会去加载下一页的数据。

const ScrollLoader: React.FC<IScrollLoaderProps> = ({
  inView,
  updateCompleted
}) => {
  const loaderRef = useRef(null);
  const observerRef = useRef<IntersectionObserver | null>(null);
  const cursorRef = useRef('');
  const [completed, setCompleted] = useState(false);

  const fetchMore = useCallback(() => {
    inView?.(cursorRef.current).then(res => {
      if (res.nextCursor) {
        cursorRef.current = res.nextCursor;
        if (observerRef.current && loaderRef.current) {
          // trigger the observer again
          observerRef.current.unobserve(loaderRef.current);
          observerRef.current.observe(loaderRef.current);
        }
      } else {
        setCompleted(true);
        updateCompleted?.(true);
      }
    });
  }, [inView, updateCompleted]);

  const callbackFunction: IntersectionObserverCallback = useCallback(
    entries => {
      entries.forEach(entry => {
        if (entry.isIntersecting && !completed) {
          fetchMore();
        }
      });
    },
    [completed, fetchMore]
  );

  useEffect(() => {
    const observerOptions = {
      root: null,
      rootMargin: '0px',
      threshold: 0.5
    };
    observerRef.current = new IntersectionObserver(
      callbackFunction,
      observerOptions
    );
    if (loaderRef.current) {
      observerRef.current.observe(loaderRef.current);
    }

    return () => {
      if (observerRef.current) {
        observerRef.current.disconnect();
        observerRef.current = null;
      }
    };
  }, [callbackFunction]);

  if (completed) {
    return null;
  } else {
    return (
      <div className={styles.loader} ref={loaderRef}>
        <Loading />
      </div>
    );
  }
};
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

通过 inView 属性来传入回调函数,当组件进入可视范围的时候,会调用这个函数,然后去加载下一页的数据。这里我们使用了一个 cursor 来标记当前的页码,当请求下一页的时候,会把这个 cursor 传给后端,后端会根据这个 cursor 来返回下一页的数据。当后端返回的数据中没有 cursor 的时候,说明已经到了最后一页,这时候我们不再渲染这个组件。

# 边界情况处理

当用户的屏幕非常长的时候,我们需要加载很多页的数据才能填满屏幕。但是 IntersectionObserver 在组件进入可视范围的时候只会触发一次回调,所以我们需要手动去重新触发。当组件加载完数据的时候,我们会先 unobserve,然后再重新 observe 这个组件,这样就可以触发回调函数了。

# 总结

滚动加载是一个很常见的需求,但是针对边界情况的处理很多人却忽略掉了。针对比较长的屏幕,需要我们做特殊处理才能满足需求。

# 参考

Intersection Observer API (opens new window)