1. 程式人生 > 其它 >SWIG入門文件(翻譯自官方網站)

SWIG入門文件(翻譯自官方網站)

技術標籤:翻譯其他python指令碼語言

SWIG簡要文件

前言

SWIG是使指令碼語言(Python,Java等)可以呼叫C/C++程式碼的工具。

此資料是將SWIG的官方文件擷取重要的部分,以此來入門,翻譯儘量表達原文的意思,我自己的語言都加了註釋。序號是根據官方文件寫的,也是為了方便隨時查詢原文件。

目錄

什麼是SWIG(What is SWIG?)

SWIG是一個用來把C/C++程式和Perl,Python,Ruby,Tcl等指令碼語言連線起來的介面編譯器。它的工作原理是獲取C/C++標頭檔案的宣告,並使用它們生成可以訪問底層C/C++程式碼的指令碼語言。此外,SWIG提供了大量的定製特性,讓你可以定製包裝過程以使用你的應用。

John Outsterhout(Tcl的創始人)曾經寫了一遍描述指令碼語言的好處的論文。SWIG使指令碼語言與C/C++程式碼連線起來相當容易。

SWIG有大量的用途:

  • 構建更強大的C/C++程式。使用SWIG,你可以用指令碼直譯器替換C程式中的main()函式,通過指令碼直譯器控制應用。這增加了相當的靈活性並使程式“可程式設計”。也就是說,指令碼介面允許使用者和開發人員輕鬆地修改程式的行為,而不必修改低階的C/C++程式碼。這樣做的好處有很多。實際上,考慮你每天使用的所有大型軟體包,幾乎所有的軟體包都包括特殊的巨集語言、配置語言、甚至是允許使用者定製的指令碼引擎。

  • 快速原型化和除錯。SWIG允許C/C++程式放置在用於測試和除錯的指令碼環境中。比如,你可以使用一組指令碼來測試一個庫或者使用指令碼直譯器作為互動式偵錯程式。因為SWIG不需要修改底C/C++程式碼,所以即使最終產品不依賴指令碼,也可以使用它。

  • 系統整合。指令碼語言在控制和粘接鬆耦合的軟體元件方面做工作地相當好。使用SWIG,不同的C/C++程式可以轉換為指令碼語言的擴充套件模組。這些模組可以被組合成新的有趣的應用。

  • 指令碼語言擴充套件模組的構建。SWIG可用於將常見的C/C++庫轉換為用在流行的指令碼語言中的模組。。當然,在此操作之前,你需要確認沒有其他人已經建立了這種模組。

SWIG有時會被用來比較其他介面定義編譯器(IDL)進行比較,比如在CORBA和COM等系統中可以找到的IDL編譯器。儘管有一些相似之處,但SWIG的目的是讓你不必嚮應用程式新增額外的IDL規範層。如果實在有的話,它更多的是一個快速應用程式開發和原型工具。具體來說:

  • ISO C/C++語法,SWIG解析已經被一些特殊指令擴充套件的ISO C++。因此,介面通常是通過抓取標頭檔案並稍微調整它來構建的。當底層C/C++程式經常被修改時,這種方法特別有用。
  • SWIG不是存根生成器(stub generator)。SWIG生成只需編譯和執行的程式碼。你不用像處理類似RPC系統那樣填寫任何存根或編寫特殊的client/server程式碼。
  • SWIG既不定義協議也不是元件框架。SWIG不定義關於軟體元件之間應該如何互動的機制或強制規則。他也不是一個專門的執行時庫或替代指令碼語言的API。SWIG只是一個程式碼生成器,它提供了C/C++與其他語言掛鉤所必須的粘合劑。
  • 被設計用於處理現有的C/C++程式碼。SWIG幾乎不需要對現有程式碼進行修改。在大多數情況下,它鼓勵您在C/C++和指令碼之劍保持清晰的界線。
  • 可擴充套件性。SWIG提供多種允許你炸掉程式的一條腿的定製選項,SWIG在這裡不是為了加強程式設計道德。

最後,值得注意的是,儘管有時會將SWIG與其他更專用的指令碼語言擴充套件構建工具(如Perl XS Python bgen)進行比較,當它的主要受眾是希望在應用程式中新增指令碼語言元件的C/C++程式設計師。正因為如此,SWIG的關注點往往與那些在指令碼語言發行版中廣泛使用而構建小模組的工具略有不同。

1.1 介紹(Introduction)

SWIG(Simplified Wrapper and Interface Generator),是一個軟體開發工具,用於將C/C++程式構建為指令碼語言介面。最初開發於1995年。SWIG通過指令碼語言自動化來大大簡化了開發,也使得開發人員和使用者可以放手去處理更重要的事情。

1.2 編譯要求(Compilation Requirements)

SWIG是用C和C++語言實現的,並以原始碼的形式公開。你需要一個可以工作的C++編譯器(比如g++)和至少一個SWIG支援的指令碼語言來執行。

ps:我的目標語言是Python,因為CentOS自帶的有,所以為了方便沒有加其他例子。

1.12.2 Unix下的安裝(Unix installation)

解壓

tar -zxvf swig-3.0.12.tar.gz

然後編譯

cd swig-3.0.12
./configure
make
make install

1.12.4 測試(Testing)

在安裝之後,如果想測試SWIG,那麼鍵入以下命令。

make -k check #此命令消耗時間久,應該是一個全面的檢查,我大概運行了20分鐘

即使此測試失敗,SWIG仍有很大機率可以正常執行。其中的一些測試也有可能會失敗,因為缺少依賴包,比如PCRE、Boost,但是這需要在配置的過程中,嚴格地分析配置輸出的結果。

2.3 目標語言(Target languages)

SWIG大體上是一個將C/C++程式碼轉換為其他各種語言的工具。對於SWIG程式碼生成器而言,C是輸入語言,其他更高級別的語言是目標語言。當SWIG執行時必須指定一個目標語言。SWIG可以被多次引用,但是目標語言不同時必須每次引用都指定目標語言。將C/C++變為不同目標語言的介面的能力是SWIG的一個核心優勢和特點。

SWIG大體上由兩部分組成。核心部分從輸入的ISO C/C++和SWIG擴充套件語法(extensions to the C/C++ standards)建立一個語法樹,然後語法樹來到第二個部分,一個生成特定於高階語言的程式碼的目標語言模組。SWIG支援許多不同的目標語言。這些語言要麼是已支援,要麼是正在實驗中。提供以上的實驗狀態,是因為不是所有的目標語言都支援。幸運的是幾乎支援大部分流行的語言。

2.4 一個SWIG例子(A SWIG example)

ps:以下程式碼在swig的解壓目錄下面,例如/swig-3.0.12/Examples/contract/simple_c下面就有,所以不用自己寫。

說明SWIG的最好方式就是寫一個例子。現在有如下C程式碼;

/* File : example.c */ 
#include <stdio.h>

int Circle (int x, int y, int radius) {
  /* Draw Circle */
  printf("Drawing the circle...\n");
  /* Return -1 to test contract post assertion */
  if (radius == 2)
    return -1;
  else
    return 1;
}

2.4.1 SWIG介面檔案(SWIG interface file)

假設你想將C程式碼中的函式和全域性變數My_variable轉換到Python中,你要先寫一個SWIG介面檔案(interface file),字尾是.i,程式碼如下。

/* File : example.i */

/* Basic C example for swig contract */
/* Tiger, University of Chicago, 2003 */

%module example

%contract Circle (int x, int y, int radius) {
require:
     x      >= 0;
     y      >= 0;
     radius >  x;
ensure:
     Circle >= 0;
}

%inline %{
extern int Circle (int x, int y, int radius);
%}

此介面檔案包括ISO C函式原型和變數宣告。%module指令是SWIG所建立的模組名。%{ %}塊裡面插入了額外程式碼,比如C標頭檔案或者其他C的宣告,會放在生成的C打包程式碼中。

2.4.2 SWIG命令列(The swig command)

以Python為例,SWIG版本為3.0.2(執行在Centos7上,ps:在win上面一直出現reference錯誤,應該是庫的問題,後來我在Centos7上則沒有出現問題,以下例子執行成功)。

[[email protected] simple_c]# swig -python example.i 
[[email protected] simple_c]# gcc -c -fpic example.c example_wrap.c -I/usr/include/python2.7/ 
[[email protected] simple_c]# gcc -shared example.o example_wrap.o -o _example.so 
[[email protected] simple_c]# python
Python 2.7.5 (default, Apr  2 2020, 13:16:51) 
[GCC 4.8.5 20150623 (Red Hat 4.8.5-39)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import example
>>> example.Circle(1,3,4)
Drawing the circle...
1
>>>

注:gcc -c -fpic是將指定的C原始檔編譯為目標檔案,然後gcc -shared … -o …,利用上面生成的目標檔案生成動態連結庫,這裡都是屬於gcc編譯器的功能。-I(大寫)指令為指定查詢標頭檔案的目錄。生成的.o檔案為目標檔案,.so為動態連結庫檔案,在win下則為.dll。

4.2 指令碼語言如何與C產生聯絡的呢?

指令碼語言是建立在一個可以執行命令和指令碼的語法分析器之上的。在這個語法分析器裡面,有一個執行命令和訪問變數的機制。一般來說,這個機制被用於實現語言的內建函式。然而,為了擴充套件直譯器,經常新增新的命令和變數。為了這個,大多數的語言都定義了用來新增新命令的特殊介面。另外,一個特殊的其他語言函式介面定義了那些新的命令應該如何接入到直譯器中。

一般來說,當新增新命令到指令碼直譯器中時,需要做兩件事:第一,需要寫一個特殊的“打包”函式,用來將直譯器和底層C函式像膠水一樣黏在一起;第二,需要提供引數、函式名等的詳細打包資訊到直譯器。

接下來的小節描述了這個過程。

4.2.1 打包函式(Wrapper functions)

現假設有如下C程式碼:

int fact(int n){
 if(n<=1)
 	return 1;
 else
 	return n*fact(n-1);
}

為了使指令碼語言訪問到此函式,有必要寫一個“打包”函式。“打包”函式必須做一下幾個事情:

  • 收集函式的引數並保證有效
  • 呼叫這個C函式
  • 將返回值轉換為指令碼語言可識別的形式

例如,Tcl打包函式如下所示:

int wrap_fact(ClientData clientData, Tcl_Interp *interp, int argc, char *argv[]) { 
    int result; 
    int arg0; 
    if (argc != 2) { 
        interp->result = "wrong # args"; 
        return TCL_ERROR;
        }
    arg0 = atoi(argv[1]); 
    result = fact(arg0); 
    sprintf(interp->result, "%d", result); 
    return TCL_OK; 
}

只要建立了一個打包函式,最後一步就只是告知指令碼語言由這個函數了。當模組載入完成後,由語言呼叫的初始化函式完成。例如,將以上函式載入到Tcl直譯器中需要如下程式碼:

int Wrap_Init(Tcl_Interp *interp) { 
    Tcl_CreateCommand(interp, "fact", wrap_fact, (ClientData) NULL, (Tcl_CmdDeleteProc *) NULL); 
    return TCL_OK; 
}

執行完之後,Tcl馬上就會有一個新的叫做“fact”的命令,和其他Tcl命令一樣的。

儘管只描述了將新函式新增到Tcl的過程,新增新函式到Perl和Python的過程也是大體相同的。都是需要寫出特殊的打包器和額外的初始化程式碼。只有一些特殊細節會有不同。

4.2.2 變數連結(Variable linking)

變數連結指的是將C/C++全域性變數對映到指令碼語言直譯器中的問題。例如,假設有如下變數:

double Foo=3.5;

從指令碼訪問此變數時可能會以如下這種方式(以Perl為例):

$a = $Foo * 2.3;	# 宣告
$Foo = $a + 2.0;	# 賦值

為了提供這種存取,變數一般使用get/set函式來管理。例如,不管在什麼時候取變數的值,必須呼叫"get"函式。同樣的,在改變變數的值時,也必須呼叫set函式。

在許多語言中,get/set函式是進行宣告和賦值的操作。因此,給 F o o 賦 值 會 暗 中 調 用 g e t 函 數 , 輸 入 Foo賦值會暗中呼叫get函式,輸入 Foo調getFoo = 4則會呼叫底層set函式來改變其值。

4.2.3 常量(Constants)

大多數情況下,一個C程式或者庫也許會定義如下的一系列常量。例如:

#define RED   0xff0000 
#define BLUE  0x0000ff 
#define GREEN 0x00ff00

為了使常量可用,它們的值儲存在指令碼語言變數中,如 R E D , RED, REDBLUE,$GREEN。幾乎所有的指令碼語言都提供了建立變數的C函式,所以儲存常量是簡單的工作。

4.2.4 結構和類(Structures and classes)

雖然指令碼語言訪問簡單的函式和變數沒有問題,但是訪問C/C++的機構和變數卻存在不同的問題。這是因為結構的實現是極大的和資料表現形式和佈局掛鉤的。另外,具體的語言特色是很難對映到直譯器的。例如,C++中的繼承在Perl的介面中是如何表示的呢?

解決結構最直接的方法是實現一系列可以隱藏資料底層表示的訪問器函式。例如:

struct Vector { 
Vector(); 
~Vector(); 
double x, y, z; 
};

可以轉化為如下形式的函式:

Vector *new_Vector(); 
void delete_Vector(Vector *v); 
double Vector_x_get(Vector *v); 
double Vector_y_get(Vector *v); 
double Vector_z_get(Vector *v); 
void Vector_x_set(Vector *v, double x); 
void Vector_y_set(Vector *v, double y); 
void Vector_z_set(Vector *v, double z);

現在,直譯器會如下這樣使用:

% set v [new_Vector] 
% Vector_x_set $v 3.5 
% Vector_y_get $v 
% delete_Vector $v
% ...

因為訪問函式提供了訪問物件內部的機制,所以直譯器不必知道Vector中的具體實現。

4.2.5 代理類(Proxy classes)

在具體的例子中,有可能會使用低級別的訪問函式來建立代理類,或者叫影子類(shadow class)。代理類是由指令碼語言生成的一種特殊的物件,以一種看起來是原生結構的方式(代理真正的C++類)訪問C/C++類(或結構)。例如,有如下C++宣告程式碼:

class Vector { 
public: Vector(); 
~Vector(); 
double x, y, z; 
};

代理類機制可以通過直譯器以一種更自然的方式訪問結構。例如,Python會這樣子用:

>>> v = Vector() 
>>> v.x = 3 
>>> v.y = 4 
>>> v.z = -13 
>>> ... 
>>> del v

同樣的,Perl5會這樣用:

$v = new Vector; 
$v->{x} = 3; 
$v->{y} = 4; 
$v->{z} = -13;

當使用代理類時,實際上有兩個物件真正的在工作,一個是指令碼語言的,一個是底層C/C++物件。

4.3 建立指令碼語言擴充套件(Building scripting language extensions)

用指令碼語言使用C/C++應用的最後一步就是新增你自己的擴充套件到指令碼語言中。這裡主要有兩種方法,優先選擇的方法是以共享庫的形式建立動態可載入擴充套件(dynamically loadable extension),另外就是可以重編譯指令碼語言直譯器,然後將擴充套件新增到其中。

4.3.1 共享庫和動態載入(Shared libraries and dynamic loading)

為了建立共享庫或DLL,就要檢視你的編譯器和連結器的手冊。然而,在一些平臺上此過程會這樣:

# Build a shared library for Solaris 
gcc -fpic -c example.c example_wrap.c -I/usr/local/include ld -G example.o example_wrap.o -o example.so 
# Build a shared library for Linux 
gcc -fpic -c example.c example_wrap.c -I/usr/local/include 
gcc -shared example.o example_wrap.o -o example.so

為了使用共享庫,你可以使用指令碼語言的互動式命令(load, import, use, etc…)。此會引入你的模組並允許你開始使用它。如下例:

% load ./example.so 
% fact 4 
24
%

當使用C++語言時,建立共享庫的過程也許會更加複雜,因為C++模組需要額外程式碼保證正確的操作。在很多機器上,你可以由以上方式來建立共享的C++模組,但需要改變一些連結行,如下:

c++ -shared example.o example_wrap.o -o example.so #注:在Linux平臺應該是g++

4.3.2 連結共享庫(Linking with shared libraries)

當將擴充套件構建為共享庫時,你的擴充套件經常會依賴於你的機器上的其他共享庫。為了使共享庫執行,需要找到所有正在執行的庫。否則將會得到一個如下的錯誤:

>>> import graph 
Traceback (innermost last): 
    File "<stdin>", line 1, in ? 
    File "/home/sci/data1/beazley/graph/graph.py", line 2, in ? 
    import graphc 
    ImportError: 1101:/home/sci/data1/beazley/bin/python: rld: Fatal Error: cannot successfully map soname 'libgraph.so' under any of the filenames /usr/lib/libgraph.so:/ lib/libgraph.so:/lib/cmplrs/cc/libgraph.so:/usr/lib/cmplrs/cc/libgraph.so: 
>>>

此錯誤表示SWIG建立的擴充套件模組依賴於“libgraph.so”共享庫,但是此庫無法定位。為了解決問題,下面有幾種方法可以嘗試。

  • 連結你的擴充套件並告訴連結器需要的庫的精確位置。常見的是,可以使用一個特殊連結符號如-R,-rpath等。這並沒有在標準手冊中實現,所以要閱讀連結器的手冊以找到如何設定共享庫的搜尋目錄的方法。
  • 將共享庫放入與可執行檔案相同的目錄下。此方法在非unix平臺中需要正確的操作。
  • 在執行Python前,新增共享庫的位置到UNIX環境變數LD_LIBRARY_PATH中。儘管這很簡單,但是並不推薦使用。而是在連結時加上可選擇的屬性來設定路徑。

4.3.3 靜態連結(Static linking)

當使用靜態連結時,你使用擴充套件重建指令碼語言直譯器。此過程通常包括編譯一個小的、加了你自定義命令到語言中的主程式,然後開始解釋。然後你就將庫和程式連結在一起,以生成新的指令碼語言可執行檔案。

即使所有的平臺都支援靜態連結,但這並不是構建指令碼語言擴充套件的最好的方式。實際上,幾乎沒有實踐的理由來做這件事,而是用共享庫代替。

5 SWIG基礎(SWIG Basics)

此章節描述了SWIG的基本操作,輸入檔案的結構,以及如何處理標準ISO C宣告。C++支援在下一個章節。然而C++程式設計師應該閱讀本章節以理解基礎。各個目標語言的特殊細節會在以後的章節。

5.1 執行SWIG(Running SWIG)

帶著執行SWIG的命令列如下所示:

swig [options] filename

filename指的是SWIG介面檔案或者C/C++標頭檔案。swig -help可以檢視所有的幫助。下面是一系列常用的選項。還為每種目標語言定義了其他選項。ps:以下只擷取部分。

支援的目標語言選項
	-csharp  -生成c#打包器
	-go      -生成Go打包器
	-java	 -生成Java打包器
	-python  -生成Python打包器
常規選項
	-c++            -開啟C++處理器
	-co <file>      -在SWIG庫中檢查<file>
	-copyright      -顯示版權資訊
	-debug-csymbols -在標誌表中顯示C標誌
	-o <outfile>    -設定C/C++的輸出檔名為<outfile>
	-version        -顯示版本資訊

5.1.1 輸入格式(Input format)

作為輸入,SWIG希望檔案中含有ISO C/C++宣告和特殊SWIG指令。通常來說,一個SWIG介面檔案字尾為.i或.swg。在某些情況下,SWIG可以直接用於原始檔或原始標頭檔案。然而,這不是最經典的例子,還有一些你不想如此做的理由(將在後面說明)。

最經典的SWIG介面檔案如下所示:

%module mymodule %{
#include "myheader.h" 
%}
// Now list ISO C/C++ declarations 
int foo; 
int bar(int x);

模組名之前使用%module來表明。在%{ … %}裡面的會簡單的複製到由SWIG建立的結果打包檔案中。

5.1.2 SWIG輸出(SWIG Output)

SWIG的輸出是一個C/ c++檔案,其中包含構建擴充套件模組所需的所有打包器程式碼。SWIG也許依據目標語言會生成一些特殊檔案。預設情況下,.i為字尾的輸入檔案會轉化為file_wrap.c或file_wrap.cxx(看是否使用-c++c選項)。因此,你必須使用-o選項來改變SWIG生成的打包檔名。在某些情況下,編譯器根據檔案字尾名來確定源語言(C,C++等)。因此,當你不想使用預設設定時,必須使用-o選項來改變SWIG打包檔案的字尾名。例如:

swig -c++ -python -o example_wrap.cpp example.i

SWIG建立的C/C++輸出檔案通常包含為目標語言構建擴充套件模組所需的所有內容。

也可以使用-outdir選項來指定Python檔案生成的目錄,預設目錄是在當前生成的C/C++檔案目錄下。如:

swig -c++ -python -outdir pyfiles -o cppfiles/example_wrap.cpp example.i

如果制訂了cppfiles和prfiles,那麼生成的檔案會如下所示:

cppfiles/example_wrap.cpp
pyfiles/example.py

如果使用的-outcurrentdie選項(沒有-o),SWIG就像經典C/C++編譯器一樣在當前目錄下生成檔案。沒有加這個選項,那麼預設輸出目錄就是輸入檔案的目錄。如果-o和-outcurrentdie同時使用,-outcurrentdir會被忽略,如果不被-outdir覆蓋,那麼語言檔案的輸出目錄就是和C/C++檔案相同的目錄。

5.1.3 註釋(Comments)

C和C++風格的註釋可以在介面檔案的任何位置出現。在SWIG的早期版本中,註釋被用來生成文件檔案。然而,此功能正在修復中並會在之後的SWIG版本中。

5.1.4 C前處理器(C Processor)

像C一樣,SWIG使用一個增強的C前處理器來預處理所有的輸入檔案。所有標準的預處理風格都支援,包括檔案包含,條件編譯和巨集命令,#include會被忽略,除非使用了-includeall命令列選項。因為SWIG有時會處理原始C標頭檔案,所以禁用了#include。在這種情況下,你僅僅是想讓擴充套件模組包含提供的標頭檔案中的函式,而不是其中包含的所有內容(即系統標頭檔案、C庫函式等)。

還應該注意的是,SWIG前處理器會跳過%{…%}中的內容。此外,前處理器包括了大量比常規C前處理器更強大的巨集指令處理。這些擴充套件在“Preprocessor”章節中有描述。

5.1.5 SWIG指令(SWIG Directives)

大部分SWIG操作都由特殊的指令控制,這些指令前有一個“%”,以區別於普通的C宣告。這些指令用於給SWIG提示或以某種方式改變SWIG的解析行為。

因為SWIG指令不是合法的C語法,所以通常不可能將指令包括在標頭檔案中。然而,SWIG指令在使用條件編譯的情況下可以包含在C標頭檔案中:

/* header.h --- Some header file */
/* SWIG directives -- only seen if SWIG is running */
#ifdef SWIG
%module foo
#endif

SWIG是在解析輸入檔案時由SWIG定義的特殊預處理符號。

5.3.3 派生類,結構體,類(Derived types, structs, and classes )

所有其他的型別(結構體,類,陣列等),SWIG應用一個非常簡單的原則:

其他的都是指標

換句話說,SWIG使用引用來控制其他的所有東西。這個規則很有用,因為大多數C/C++程式設計師大量使用指標,而SWIG可以使用型別檢查指標機制,這已經用於將指標轉換為基本型別。

儘管聽起來複雜,但是其實非常簡單。假設有如下介面檔案程式碼:

%module fileio
FILE *fopen(char *, char *);
int fclose(FILE *);
unsigned fread(void *ptr, unsigned size, unsigned nobj, FILE *);
unsigned fwrite(void *ptr, unsigned size, unsigned nobj, FILE *);
void *malloc(int nbytes);
void free(void *);

在此檔案中,SWIG不知道FILE是什麼,但是因為這是一個指標,所以是什麼並不真正影響。如果你想將此模組打包到Python中,你可以根據你的想法使用以下函式:

# Copy a file
def filecopy(source, target):
    f1 = fopen(source, "r")
    f2 = fopen(target, "w")
    buffer = malloc(8192)
    nbytes = fread(buffer, 8192, 1, f1)
    while (nbytes > 0):
    fwrite(buffer, 8192, 1, f2)
    nbytes = fread(buffer, 8192, 1, f1)
    free(buffer)

在這種情況下,f1,f2和buffer都是包含C指標的不透明的物件。裡面的值是什麼並不影響,不知道值是什麼,程式仍然可以很好的工作。