跨平臺: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

1.設置NDK工具集目錄(入口build/build_for_android.sh編譯腳本中抽取的NDK對應的CPU指令架構工具集)

2.設置openssl輸出目錄(依賴openssl)

3.設置目標機器指令集

4.編譯

關鍵代碼(限于篇幅只貼出部分腳本):

# 自己的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();

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容