跨平台:libcurl+openssl编译(Mac、Android、Windows、MD/MT模式、XP)

前提概要

众所周知,http/https是当下开发应用程序时,网路部分不可或缺的部分,我们可以基于socket自己来实现,因为http/https本身是基于TCP实现的应用层协议(位于网络模型的第7层)。但随着行业的发展,https加密、业内非标准http协议的推广(CDN非标准协议)等这些部分,都需要耗费大量的开发成本,基于socket自己实现http/https的方案,成本上已经难以接受,选择开源的成熟方案是当下业内的共识。而curl是http/https最成熟的开源方案,其兼顾稳定性和易用性、跨平台性,是作为底层库的首选。当然其他一体化底层解决方案也是不错的选择,例如Mars(微信开源框架),Qt等,这里我们仅在单一http/https方案这一选择中来做探讨。

curl虽然易于使用,但在各平台编译上,有不少晦涩难懂的地方,也是它对于使用者来说最大的障碍,这篇文章旨在消除这些障碍,拨开云雾,一站式解决各平台编译问题,从而将大家宝贵的精力从中抽出,用在更有价值的事情上。

参考资料:

https://curl.haxx.se/docs/install.html
http://p-nand-q.com/programming/windows/building_openssl_with_visual_studio_2013.html
https://wiki.openssl.org/index.php/Main_Page

基本脉络

这里是我个人理解下来,需要大家提前搞懂的几个点,整理出脉络图,以便理解。和代码阅读类似,我们先观其行,然后再达其意,有利于各个击破,如果能接触到一两个有意思的技术历史,那也不失为过程中的风景,本篇文章也会按照各平台来逐一介绍和阐述。
在这里插入图片描述

 

整体工程

请务必下载下来如下链接中整体curl编译工程,然后再针对性阅读后续介绍

百度云盘:https://pan.baidu.com/s/1yXdqiUMBiHqeyVktlFUQ9A
腾讯微云:https://share.weiyun.com/5t9cdnJ

针对整体工程,我们分如下几部分做介绍

1.mac/ios编译
2.windows编译
3.android编译
4.openssl多线程安全

工程目录结构如下:
在这里插入图片描述

一、Mac编译

主要参考curl官方文档:

https://curl.haxx.se/docs/install.html
iOS的编译,这里我稍作说明,由于个人精力的缘故,没有完整实践过,之前看官方文档的时候,大致看到方法应该是类似的,由于移动平台CPU多种多样,这里是否有编译上的差异,我还未做考证。总之,再麻烦总不会麻烦过Android(文章后半段大家会感觉到这一点),请大家阅读好官方文档,我们要做的大多数情况下只是保持好正确的坐姿,设置好编译参数,然后正确调用编译命令。

注意事项
需要更新到xcode9.4.1以上版本,curl-7.63.0版本在xcode9.2.1版本编译会报如下错误:

Undefined symbols for architecture x86_64:
    “_SSLCopyALPNProtocols”, referenced from:
            _darwinssl_connect_step2 in libcurl.a(libcurl_la-darwinssl.o)
    “_SSLSetALPNProtocols”, referenced from:
            _darwinssl_connect_common in libcurl.a(libcurl_la-darwinssl.o)
ld: symbol(s) not found for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)
make[2]:[curl] Error 1
make[1]:[install-recursive] Error 1
make: [install-recursive] Error 1

编译脚本build/libcurl/build_for_mac.sh执行的步骤:

1.解压源码:curl-7.63.0.tar.gz
2.编译libcurl

关键代码(限于篇幅只贴出部分脚本):其中current_path是当前脚本执行路径,是编译输出路径,也可以配置为自定义的输出路径

export MACOSX_DEPLOYMENT_TARGET=“10.6”

# buid configure
./buildconf
./configure --prefix=$current_path/out
                  --disable-shared
                  --enable-static
                  --with-darwinssl
                  --enable-threaded-resolver
                  --disable-ldap
                  --disable-ldaps

# workaround still works though: make CFLAGS=-Wno-error for buid bug before v7.55.1
# the build error is:connectx’ is only available on macOS 10.11 or newer
#make CFLAGS=-Wno-error
make

# install
make install

在win/linux/android下使用openssl,在mac/ios下用apple体系下ssl实现(Apple’s SSL/TLS implementation)

通过指定编译参数来指明:–with-darwinssl, remark: it is not necessary to use the option --without-ssl

 

二、Windows编译

windows上libcurl的编译,可以参考libcurl源码下

winbuild/BUILD.WINDOWS.txt

准备环境:

1.安装ActivePerl:官网下载http://www.activestate.com/activeperl/downloads
2.安装nasm:官网下载http://www.nasm.us,附件中(build/build_depend_tools)也有安装包,并在系统环境 变量中添加nasm安装路径(也可以使用附件中包含的批处理文件添加
3.安装python:确认系统环境变量中是否已自动添加python路径,若没有手动添加

 
1.编译入口

编译脚本build/build_for_win.bat执行步骤:

1.设置7zip环境变量,用于解压源码(整体工程压缩包中,带有7zip,curl/7-Zip路径下)
2.编译openssl
3.编译libcurl
4.删除openssl临时生成文件
5.删除libcurl临时文件

关键代码(限于篇幅只贴出部分脚本):

@echo off
@set workdir=%cd%
@set sevenzip=%workdir%\7-Zip\7z.exe

:: build openssl
@cd %workdir%\openssl
@call build_for_win.bat %sevenzip%
@cd %workdir%

:: build libcurl
@cd %workdir%\libcurl
@call build_for_win.bat %sevenzip%
@cd %workdir%

:: delete openssl temp files
@if exist %workdir%\openssl\openssl-1.0.2l (rd /s /q “%workdir%\openssl\openssl-1.0.2l”) else echo can not find openssl-1.0.2l dir
@if exist %workdir%\openssl\output_lib (rd /s /q “%workdir%\openssl\output_lib”) else echo can not find output_lib dir

:: delete libcurl temp files
@if exist %workdir%\libcurl\curl-7.63.0 (rd /s /q “%workdir%\libcurl\curl-7.63.0”) else echo can not find curl-7.63 dir

@echo on

 
2.首先编译openssl

编译脚本build/openssl/build_for_win.bat执行步骤:

1.设置VC环境变量(这里使用的是VS2015,可以根据自身需要自定义修改)
2.解压源码:openssl-1.0.2l.tar.gz
3.设置输出目录
4.代码工程相关设置
5.将/MD设置为/MT模式
  这一步根据自己工程的需要来做,如果应用程序其他模块都是/MD模式,则不需要执行这一步
  另外,由于openssl中没有提供脚本选项来自动生成/MT工程,所以只能替换生成的.mak中对应选项
6.编译
7.同步生成文件到目标路径,脚本中是对应工程kernel的输出路径,这里可以根据自身工程需要修改为自定义路径
8.同步生成的.pdb文件目标路径,脚本中是对应工程kernel的输出路径,这里可以根据自身工程需要修改为自定义路径
  由于openssl安装脚本中没有提供pdb文件安装选项,所以这里需要额外从openssl临时生成路径下拷贝出来

关键代码(限于篇幅只贴出部分脚本):

:: 3)generate VC project config
@perl configure VC-WIN32 --prefix=%outputlib%
@call ms\do_ms.bat
@call ms\do_nasm.bat

:: 4)replace “/MD” to “/MT” in ms/ntdll.mak
@setlocal enabledelayedexpansion
@set ntdll_mak_file=%currentPath%\openssl-1.0.2l\ms\ntdll.mak
@set ntdll_mak_file_temp=%currentPath%\openssl-1.0.2l\ms\ntdll_temp.mak if exist %ntdll_mak_file_temp% (del %ntdll_mak_file_temp%) else echo create temp file ntdll_temp.mak"
for /f “delims=” %%i in (%ntdll_mak_file%) do (
    set str=%%i
    set str=!str:/MD=/MT!
    echo !str!>>%ntdll_mak_file_temp% )
@move /y “%ntdll_mak_file_temp%” “%ntdll_mak_file%”
@endlocal enabledelayedexpansion

:: 5)build
@nmake -f ms\ntdll.mak
@nmake -f ms\ntdll.mak install @cd %currentPath%

 
3.然后设置openssl依赖,编译curl

脚本build/libcurl/build_for_win.bat执行步骤:

1.设置VC环境变量(这里使用的是VS2015,可以根据自身需要自定义修改)
2.解压源码:curl-7.63.0.tar.gz
3.支持Windows XP版本
  VS2010以后,XP系统需要单独设置才能支持,若不需要,可以在curl/build/build_for_win.bat中去掉“@call build_for_win.bat %sevenzip% enable_xp”中enable_xp参数即可
  VS2015,推荐使用,兼容性好,工程中curl/build/build_for_win.bat编译脚本中默认开启了XP支持,这种情况下编译脚本自动化不会出错
  VS2013,不推荐,curl源码中对自动化编译支持有兼容性问题,打开了XP支持参数后,VS2013需要手动编译才能编译通过,VS2013工程在curl/build/libcurl/curl-7.63.0/project/windows/VC12路径下
4.编译这里使用的是/MT模式,如果需要使用/MD模式,择修改“RTLIBCFG=static” 为 “RTLIBCFG=dll”
  RTLIBCFG=static,表示libcurl是/MT
  RTLIBCFG=dll,表示libcurl是/MD
5.同步生成文件到目标路径下,脚本中是对应工程kernel的输出路径,这里可以根据自身工程需要修改为自定义路径

关键代码(限于篇幅只贴出部分脚本):

:: 2) support Windows XP (add build command into “winBuild/MakefileBuild.vc”)
if “%supportXP%”=="enable_xp” (
    echo modify “winbuild/MakefileBuild.vc” to support windows xp
    python %currentPath%\build_for_win_support_xp.py %currentPath%\curl-7.63.0\winbuild\MakefileBuild.vc
)

:: 3)build
@cd %currentPath%\curl-7.63.0\winbuild
@nmake /f Makefile.vc WITH_DEVEL=…/…/…/openssl/output_lib mode=dll VC=14 RTLIBCFG=static WITH_SSL=dll GEN_PDB=yes DEBUG=no MACHINE=x86
@cd %currentPath%

:: 4)sync build result to kernel
@set output=%currentPath%\curl-7.63.0\builds\libcurl-vc14-x86-release-dll-ssl-dll-ipv6-sspi
@copy /y “%output%\bin\libcurl.dll” “%currentPath%…\output\bin”
@copy /y “%output%\lib\libcurl.lib” “%currentPath%…\output\lib”
@copy /y “%output%\lib\libcurl.pdb” “%currentPath%…\output\bin”

 

三、Android编译

https://wiki.openssl.org/index.php/Android
https://wiki.openssl.org/index.php/FIPS_Library_and_Android

google中搜索 “openssl” “android” “wiki”关键字,从而找到权威文档https://wiki.openssl.org/index.php/Android
关于NDK相关环境变量设置,参考权威文档中Setenv-android.sh

 
1.编译入口

脚本build/build_for_android.sh执行步骤:

1.声明环境变量:NDK、NDK根目录、NDK版本、CPU指令架构(arm/x86/…)
2.抽取NDK对应的CPU指令架构工具集(编译时需要用到)
3.编译openssl
4.编译libcurl
5.删除抽取出来的NDK指令架构工具集
6.同步生成文件到目标路径下,脚本中是对应工程kernel的输出路径,这里可以根据自身工程需要修改为自定义路径

NDK工具集抽取:

这里首先需要有一些技术上的概念理解,才能做到真正的掌握,技术上无惑于心太重要,毕竟我个人的目的也不仅仅是让大家去使用我提供的编译脚本,否则我没有必要赘述这篇文章了

主机编译:一般来说,大多数可执行程序的编译都是主机编译
                  例如,windows上VS编译windows程序,mac上xcode编译mac程序,都是主机编译。
交叉编译:与主机编译相对应,在其他系统上编译出目标系统上的可执行程序
                  例如,目前在Android机器上没有完备的编译开发环境,从而导致只能在其他系统上编译Android应用,这种就是典型的交叉编译。

在NDK之中,根据Android系统的CPU指令架构的不同,包含了能够实现Android应用程序交叉编译的各种工具集,以16b版本的NDK为例,其工具集目录是这样:
android-ndk-r16b
           | - toolchains
                      | - arm-linux-androideabi-4.9
                      | - aarch64-linux-android-4.9
                      | - mipsel-linux-android-4.9
                      | - mips64el-linux-android-4.9
                      | - renderscript
                      | - x86-4.9
                      | - x86_64-4.9
                      | - llvm

一般来说,跨平台项目的编译,编译脚本中兼容性做得好的话,使用者是不需要关心当前应该使用哪一种交叉编译工具集的,但很可惜,openssl这里需要我们关心这一点,这也是当时我在处理这块时,一开始遇到障碍与困惑的地方,也花了不少时间,直到搞清楚了这些基本技术概念的来龙去脉后,才找到正确的方法。NDK这块,有太多的东西,这里我们需要做到怎样的程度呢,对于这种类似的问题,我个人一般秉持一个原则:“代码的编写,通透到底为好,而工具的使用,则是技术盲区的知识补充到刚好够用即可”,毕竟人的精力有限,兴趣之上就看个人了。最后,言归正传,NDK本身提供了比较完善的工具集抽取命令,我们这里的编译脚本中也是简单调用,而后抽取出来的工具集路径在openssl编译的脚本中正确设置给环境变量即可。

关键代码(限于篇幅只贴出部分脚本):

echo 抽取NDK指令集目录
ndk_toolchain_dir="$work_dir/ndk_toolchain"
rm -rf $ndk_toolchain_dir
$ndk_dir/build/tools/make_standalone_toolchain.py --arch $target_cpu --api $ndk_ver --stl gnustl --install-dir=$ndk_toolchain_dir --force

echo 编译openssl
bash $work_dir/openssl/build_for_android.sh $ndk_root $ndk_toolchain_dir $target_cpu

echo 编译libcurl
bash $work_dir/libcurl/build_for_android.sh $ndk_toolchain_dir $work_dir/openssl $target_cpu

echo 删除NDK临时目录$ndk_toolchain_dir
rm -rf $ndk_toolchain_dir

echo 同步libcurl和openssl头文件
cp $work_dir/libcurl/out/$target_cpu/include/curl/
.h $work_dir/…/
cp $work_dir/openssl/out/$target_cpu/include/openssl/.h $work_dir/…/openssl

 
2.首先编译openssl

脚本build/openssl/build_for_android.sh执行步骤:

1.设置NDK相关环境变量,内部编译时会用到,
  这部分主要参考https://wiki.openssl.org/index.php/Android中的Setenv-android.sh
2.编译

关键代码(限于篇幅只贴出部分脚本):

arch_target=arch-x86
if [ $target_cpu == “x86” ]; then
    arch_target=android-x86
fi
if [ $target_cpu == “arm64” ]; then
    arch_target=android-armv7
fi
if [ $target_cpu == “arm” ]; then
    arch_target=android-armv7
fi

./Configure $arch_target no-shared no-comp no-hw no-engine --prefix=$ssl_path --openssldir=$ssl_path --sysroot=$CROSS_SYSROOT -D__ANDROID_API__=18 -isystem$ANDROID_SYSTEM
if [ $? != 0 ]; then
    exit 1
fi

make depend
if [ $? != 0 ]; then
    exit 1
fi

make all
if [ $? != 0 ]; then
    exit 1
fi

make install_sw
if [ $? != 0 ]; then
    exit 1
fi

 
3.然后设置openssl依赖后,编译curl

脚本build/libcurl/build_for_android.sh执行步骤:

1.解压源码:curl-7.63.0.tar.gz
2.设置NDK工具集目录(入口build/build_for_android.sh编译脚本中抽取的NDK对应的CPU指令架构工具集)
3.设置openssl输出目录(依赖openssl)
4.设置目标机器指令集
5.编译

关键代码(限于篇幅只贴出部分脚本):

# 自己的android-toolchain(NDK针对特定配置抽取出来的独立目录)
export ANDROID_HOME=$ndk_toolchain_dir

# openssl的输出目录
export CFLAGS="-isystem$openssl_dir/out/$target_cpu/include"
export LDFLAGS="-L$openssl_dir/out/$target_cpu/lib"
export TOOLCHAIN=$ANDROID_HOME/bin

# 设置目标机器指令集
arch_flags="-march=i686 -msse3 -mstackrealign -mfpmath=sse"
arch=arch-x86
tool_target=i686-linux-android
host_os=i686-linux-android
if [ $target_cpu == “x86” ]; then
    arch_flags="-march=i686 -msse3 -mstackrealign -mfpmath=sse"
    arch=arch-x86
    tool_target=i686-linux-android
    host_os=i686-linux-android
fi
if [ $target_cpu == “arm” ]; then
    arch_flags="-march=armv7-a -msse3 -mstackrealign -mfpmath=sse"
    arch=arch-arm
    tool_target=arm-linux-androideabi
    host_os=arm-androideabi-linux
fi
if [ $target_cpu == “arm64” ]; then
    arch_flags="-march=armv8 -msse3 -mstackrealign -mfpmath=sse"
    arch=arch-arm
    tool_target=arm-linux-androideabi
    host_os=arm-androideabi-linux
fi
echo 当前CPU指令集匹配arch为"$arch",arch_flags为$arch_flags

export TOOL=$tool_target
export ARCH_FLAGS=$arch_flags
export ARCH=$arch
export CC=$TOOLCHAIN/$TOOL-gcc
export CXX=$TOOLCHAIN/${TOOL}-g++
export LINK=${CXX}
export LD=$TOOLCHAIN/${TOOL}-ld
export AR=$TOOLCHAIN/${TOOL}-ar
export RANLIB=$TOOLCHAIN/${TOOL}-ranlib
export STRIP=$TOOLCHAIN/${TOOL}-strip
export CPPFLAGS="-DANDROID -D__ANDROID_API__=18"
export LIBS="-lssl -lcrypto"
export CROSS_SYSROOT=$TOOLCHAIN/sysroot

cd $source_dir
./configure --prefix=$current_path/out/$target_cpu
                 --exec-prefix=$current_path/out/$target_cpu
                 —bindir=$TOOLCHAIN
                 —sbindir=$TOOLCHAIN
                 —libexecdir=$TOOLCHAIN
                 —with-sysroot=$CROSS_SYSROOT
                 —host=$host_os
                 —enable-ipv6
                 —enable-threaded-resolver
                 —disable-dict
                 —disable-gopher
                 —disable-ldap
                 —disable-ldaps
                 —disable-manual
                 —disable-pop3
                 —disable-smtp
                 —disable-imap
                 —disable-rtsp
                 —disable-smb
                 —disable-telnet
                 —disable-verbose

make install

 

四、openssl多线程安全

openssl在多线程这块,有些历史因素,导致不同版本应用层需要做的事情不一样:

参考文档:https://www.openssl.org/blog/blog/2017/02/21/threads/
1.v1.0.2和之前的版本,多线程安全需要应用层自己实现,在openssl/crypto.h中有预留实现接口位置
  文档中示例程序th-lock.c有做说明
2.v1.1.0版本之后,多线程安全加锁的实现,从运行时转到了编译期间
  编译时启用多线程安全参数,则openssl会将各平台加锁实现打包进来

我们这里使用的版本是v1.0.2l版本,所以多线程安全部分需要应用层自己实现,虽然如此,openssl其实已经做了很多,我们只需要实现特定的几个位置的代码即可,具体代码如下:

 
加锁实现部分:

#include “openssl/crypto.h”
#include “openssl/err.h”

#if defined(WIN32)
#define MUTEX_TYPE HANDLE
#define MUTEX_SETUP(x) (x) = CreateMutex(NULL, FALSE, NULL)
#define MUTEX_CLEANUP(x) CloseHandle(x)
#define MUTEX_LOCK(x) WaitForSingleObject((x), INFINITE)
#define MUTEX_UNLOCK(x) ReleaseMutex(x)
#define THREAD_ID GetCurrentThreadId()
#else
#include <pthread.h>
#define MUTEX_TYPE pthread_mutex_t
#define MUTEX_SETUP(x) pthread_mutex_init(&(x), NULL)
#define MUTEX_CLEANUP(x) pthread_mutex_destroy(&(x))
#define MUTEX_LOCK(x) pthread_mutex_lock(&(x))
#define MUTEX_UNLOCK(x) pthread_mutex_unlock(&(x))
#define THREAD_ID pthread_self()
#endif

static MUTEX_TYPE mutexArray = NULL;
static int32_t nNumLocks = 0;

static void locking_function(int mode, int n, const char * file, int line)
{
    if (n >= nNumLocks) {
        return;
    }

    if (mode & CRYPTO_LOCK) {
        MUTEX_LOCK(mutexArray[n]);
    } else {
        MUTEX_UNLOCK(mutexArray[n]);
    }
}

static unsigned long threadId_function(void)
{
    return ((unsigned long)THREAD_ID);
}
namespace OpenSSLThreadLock
{
    void OpenSSLLock_Setup(void)
    {
        nNumLocks = CRYPTO_num_locks();

#ifdef _MSC_VER
        mutexArray = (MUTEX_TYPE
)OPENSSL_malloc(nNumLocks * sizeof(MUTEX_TYPE));
#else
        mutexArray = (MUTEX_TYPE
)malloc(nNumLocks * sizeof(MUTEX_TYPE));
#endif

        if (!mutexArray) {
             return;
         }

        for (int32_t i = 0; i < nNumLocks; ++i) {
            MUTEX_SETUP(mutexArray[i]);
        }

        CRYPTO_set_id_callback(threadId_function);
        CRYPTO_set_locking_callback(locking_function);
    }

    void OpenSSLLock_Cleanup(void)
    {
            if (!mutexArray) {
                return;
            }

            CRYPTO_set_id_callback(NULL);
            CRYPTO_set_locking_callback(NULL);
            for (int32_t i = 0; i < CRYPTO_num_locks(); ++i) {
                MUTEX_CLEANUP(mutexArray[i]);
            }

#ifdef _MSC_VER
            OPENSSL_free(mutexArray);
#else
            free(mutexArray);
#endif
            mutexArray = NULL;
    }
}*

 
调用部分则比较简单,只需要在libcurl模块整体初始化和退出调用对应接口即可

1.http模块初始化时
OpenSSLThreadLock::OpenSSLLock_Setup();

2.http模块退出时
// curl_global_cleanup();之前调用
OpenSSLThreadLock::OpenSSLLock_Cleanup();

更多相关推荐

curl android 编译,跨平台:libc...

前提概要众所周知,http/https是当下开发应用程序时,网路部分不可或缺的部分,我们可以基于so...

继续阅读

跨平台:GN实践详解(ninja, 编...

目录一、概览二、跨平台代码编辑器三、GN入门四、示范工程五、关键细节六、结语[编译器选项] ...

继续阅读

python交叉编译跨平台_交叉编译P...

Ihaveaproblemwithcross-compilingnetifacesextensionunderBuildrootLinuxdistroforARM(Python...

继续阅读

跨平台基础网络框架Mars初探

前言对于新派单通知、订单时效变更通知等需要及时反馈给用户的消息,目前点我达骑手的解决方案...

继续阅读

Windows平台 Qt 5.9 VS2017 静态...

Windows平台Qt5.9VS2017静态编译Windows平台Qt59VS2017静态编译总览ICUOpenSSLQt编译总览这篇...

继续阅读

Android和JavaWeb通性跨平台之C...

Android和JavaWeb通性跨平台之C的应用我们都知道Java是一门跨平台语言,能够跨平台的原因是虚...

继续阅读