1. 程式人生 > >CSharp與C/CPP混編技術簡要說明

CSharp與C/CPP混編技術簡要說明

CSharp與C/CPP混編 (mixing coding with CSharp and C/CPP)

混合程式設計技術,是一種在工程上廣泛存在,並且已經發展了很多年的“古老”技術,常見的混合程式設計,有Java/C的程式設計,也有今天要提到的CSharp/C的組合形式。說到底,我們使用混合程式設計技術,是因為在我們的實際生產過程中,經常會遇到一種語言無法有效處理全部問題的情況。其中一個常見的問題就是執行效率和程式設計效率之爭。

以C/CPP為例,主流的底層技術框架,例如CUDA,OpenGL,OpenCV等,都是直接支援C/CPP的,而其他由C/CPP語言發展起來的新技術語言,也保留著C/CPP的介面。例如Matlab,具備廣泛的數學工具庫,是做數學分析和數值模擬的重要工具,但是全部計算都依賴Matlab會導致程式的執行效率異常低下,因此會把某些計算模組進行編譯,暴露介面給Matlab,使Matlab的計算模組能夠被C/CPP使用,從而更廣泛和效率的被應用在其他場景中。

關於其他語言與C/CPP的混合程式設計技術,您如果感興趣,可以自行搜尋相關資料,做點demo看看。在這裡,我將介紹CSharp與C/CPP的混合程式設計技術,以及一些重要的注意要點。

技術要求(pre-requirements)

您首先要具備一定的C/CPP程式設計經驗,在Windows平臺上,需要熟練使用類似Visual Studio這樣的開發工具,然後能夠使用Visual Studio釋出native 動態連結庫(dll)。

同時應該具備熟練的記憶體管理知識,明白程式中棧(stack)和堆(heap)的特點和應用方式,明白如何在記憶體中建立劃分記憶體塊(malloc / new),以及如何手動回收這些記憶體(free / delete)。

編寫C/Cpp native dll(programming C/CPP native dll)

dll 也就是動態連結庫(dynamic link library)的縮寫,它是windows平臺上,“熱插拔”技術理念的產物,當我們程式需要某方面的功能時,通過dll動態的載入到我們的程式中。此外,dll,也是本文中實現在Windows平臺上CSharp/CPP混合程式設計的關鍵所在。

我們計劃在本文裡,需要實現的程式應該包含如下功能:

  • 根據使用者輸入的檔名,載入圖片(JPG/PNG/TIF)
  • 獲取檔案的一些基本資訊,如圖片的長寬,資料通道,檔案大小
  • 在螢幕上顯示檔案
  • 資源的釋放

為了演示,這些主要的功能都將使用C/CPP進行程式設計,您可以根據我提供的原始碼,自己在此基礎上做進一步的擴充套件。

首先,通過Visual Studio建立一個工程,並且第一個專案型別選擇為Visual C++,您需要調整Visual C++的工程屬性,在Properties裡,將工程生成屬性改為Dynamic Library(.dll)

然後,建議您建立一個頭檔案,並且把計劃暴露給其他使用者使用介面都放在這個標頭檔案裡,這樣做的好處是不僅你的CSharp工程可以使用這些介面,其他的C/CPP工程也可以使用這些介面,如果您按照示例規範編寫這些介面的話,其他語言也可以使用這些介面。

#pragma once

#ifndef _HEADER_H_
#define _HEADER_H_

#include "typedefine.h" /* import external headers */

#ifndef MS_CPP_API_EXPORTS
#define MS_API __declspec(dllexport) 
#else
#define MS_API __declspec(dllimport) 
#endif


#ifdef __cplusplus    /*C++編譯器包含的巨集,例如用g++編譯時,該巨集就存在,則下面的語句extern "C"才會被執行*/  
extern "C" {          /*C++編譯器才能支援,C編譯器不支援*/
#endif

#if _WIN32 /* cross platform coding for windows style */

    MS_API IntPtr load_image_source(const char* filename);

    MS_API void extract_image_info(int* width, int* height, int* channel);

    MS_API void display_image();

    MS_API void release();

#elif linux /* cross platform coding for linux style */

    IntPtr load_image_source(const char* filename);

    void extract_image_info(int* width, int* height, int* channel);

    void display_image();

    void release();

#endif


#ifdef __cplusplus  
}
#endif


#endif

注意(caution)!

編寫介面標頭檔案,應該儘量避免平臺專屬的函式或定義,自定義的資料型別。為避免出現記憶體對齊以及一些其他莫名的問題,介面暴露的資料型別應該儘量為基礎的資料型別如char, int, long, double, float,或者基礎的陣列形式如 int[], float[]等,如果需要暴露一些特殊的指標型別,儘量統一的轉化為void指標型別,對於需要傳遞class, struct這樣可能包含多種資料型別的變數型別,建議手工轉換為byte陣列形式,並且注意平臺之間,不同程式語言之間可能出現的資料大小端問題。

引用第三方類庫 (linking third part libraries)

通常一個較大的工程中,可能引用了超過一個的類庫。沒關係,您可以把相關類庫的載入放在另一個單獨的標頭檔案裡,並且直接載入到您的具體實現程式碼裡,您需要避免由於不同平臺、語言對於第三方類庫載入的限制,因此應該儘量避免這類第三方的呼叫暴露給其他人。

比如我們這個專案的示例,為了簡化我們的工作,我們使用到了OpenCV作為第三方庫。這類第三方庫的載入和呼叫,統一放在了一個內部可見的標頭檔案中,並且不計劃暴露給使用者使用。

#pragma once

#ifndef LINK_NECESSARY_LIBS
#define LINK_NECESSARY_LIBS
#if _WIN32
#if _DEBUG
#pragma comment(lib, "opencv_world330d.lib")
#else
#pragma comment(lib, "opencv_world330.lib")
#endif
#endif
#endif

由於這類巨集定義,主要是Windows平臺使用,所以通過巨集定義進行限制,這樣當代碼需要在Linux系統上執行時,就不會報錯了。

C/CPP的介面實現程式碼 (implementation of C/CPP interfaces)

最後,介面的實現,被放在一個cpp檔案中,它看起來是這樣的。

#include "dll_header.h"
#if _WIN32
#include "win_lib_link.h"
#endif

#include <stdio.h>
#include <opencv2/opencv.hpp>

using namespace cv;

static IntPtr frame;

#if _WIN32
MS_API
#endif
IntPtr load_image_source(const char* filename)
{
    frame = new Mat(imread(filename));
    return frame;
}


#if _WIN32
MS_API
#endif
void extract_image_info(int* width, int* height, int* channel)
{
    Mat* ptr = (Mat*)frame;
    *width = ptr->cols;
    *height = ptr->rows;
    *channel = ptr->channels();
}


#if _WIN32
MS_API
#endif 
void display_image()
{
    Mat* ptr = (Mat*)frame;
    cv::namedWindow("demo");
    cv::imshow("demo", *ptr);
    cv::waitKey(0);
}


#if _WIN32
MS_API
#endif
void release()
{
    if (frame != nullptr) delete(frame);
}

這裡你需要關注下static這個關鍵字的使用,它意味著我們通過OpenCV載入的圖片資料,會被長期的放置在記憶體的stack中,避免由於方法體的銷燬而導致的指標懸空,我們給使用者暴露了一個void*的指標,便於使用者對底層資料的操作,但建議最好不要這樣去做,使用者最好通過你構建的方法去操作留存在記憶體中的資料。

此外,由於我們暴露給使用者可操作的資料型別是基礎資料型別,因此就意味著跨平臺編譯連結的過程,出現意外的可能性被降低至可控範圍。當然,不可否認的是,這樣的方式會破壞函式體的美觀,尤其函式體需要傳回大量的基礎資料的時候,針對這樣的情況,就建議把回傳的資料塊,以Void指標的形式進行暴露,接收端對這些資料進行強型別轉換就行。

CSharp呼叫暴露的介面的方式 (call the native APIs)

實現程式碼非常簡單,如下:

namespace CSharpImportDemo
{
    class Program
    {
        [DllImport("DllCoreDemo.dll", EntryPoint = "load_image_source")]
        public static extern IntPtr LoadImage(string filename);

        [DllImport("DllCoreDemo.dll", EntryPoint = "extract_image_info")]
        public static extern IntPtr ExtractImgInfo(out int width, out int height, out int channel);

        [DllImport("DllCoreDemo.dll", EntryPoint = "display_image")]
        public static extern IntPtr DisplayImage();

        [DllImport("DllCoreDemo.dll", EntryPoint = "release")]
        public static extern IntPtr Release();


        static void Main(string[] args)
        {
            IntPtr frameMat = LoadImage("test.jpg");

            int width, height, channels;
            ExtractImgInfo(out width, out height, out channels);

            Console.WriteLine("{0} {1} {2}", width, height, channels);

            DisplayImage();

            Release();
        }
    }
}

注意(caution)!

你最好指定清楚每個方法它的介面位置(Entry point),如果有需要,還需要制定清楚編碼方式是UTF8還是其他,以及是否採用標準的呼叫形式,具體可以參考下微軟的官方說明。

針對C函式中暴露的基礎資料型別回傳,CSharp中可以通過定義關鍵字out/ref獲取到資料。並且,別忘記手動釋放記憶體,避免由於記憶體資源的不釋放,導致程式甚至系統的崩潰。

此外,還有個非常重要的地方,你一定要注意!當你釋出了64位的dll,一定記得要修改CSharp專案為64位系統,CSharp預設為32位,當呼叫的dll與目標程式不符時,會報錯無法執行。

C Structure 型別的引用

當您無論如何也需要傳遞包含多資料的Structure型別,比如示例中需要展示圖片的長、寬、高,以及圖片包含的資料長度,那麼CSharp也提供了相關方法,但是這種方法可能會出現記憶體無法對齊的問題,因此基於“簡單即是完美”的設計法則,儘量減少 struct 型別所包含的資料型別,儘量避免多重資料引用,乃至於 struct 包含邏輯方法塊這樣的情況出現。我們並不清楚微軟是否在新一代DotNet平臺提供了技術支援和保證,但是你仍然無法避免在低版本的DotNet存在異常。

首先,在我們的C程式碼中,增加需要傳遞 struct 資料型別。

struct FrameInfo
{
 int width;
 int height;
 int channels;
 size_t size;
};

然後,相關方法塊的定義如下:

MS_API void update_struct(IntPtr ptr, FrameInfo * info);

它的實現是這樣的:

#if _WIN32
MS_API
#endif
void update_struct(IntPtr frame, FrameInfo * info)
{
    if (info == nullptr) info = new FrameInfo();
    Mat* ptr = (Mat*)frame;

    info->width = ptr->cols;
    info->height = ptr->rows;
    info->channels = ptr->channels();
    info->size = info->width * info->height * ptr->elemSize1();
}

之後,編譯C/CPP工程為動態連結庫,在CSharp工程中,相關方法的引用和結構體的對應是這樣的:

[DllImport("DllCoreDemo.dll", EntryPoint = "update_struct")]
public static extern void UpdateStruct(IntPtr frame, ref FrameInfo structure);

[StructLayout(LayoutKind.Sequential)]
public struct FrameInfo
{
    public int width;
    public int height;
    public int channels;
    public long size;
};

它的呼叫方法,是這樣的:

FrameInfo info = new FrameInfo();
UpdateStruct(frameMat, ref info);

說明 (Instruction)

C/CPP工程的相關定義,沒有什麼需要特別說明的。你需要注意的是C/CPP有很多不同的型別定義,你需要清楚的知道這些不同於基礎資料型別的定義,它實際應該對應的資料型別是什麼,長度是多少。當你需要在CSharp中定義並引用這些資料的時候,那麼需要按照原定義的順序,實現對應的資料型別。

然後在CSharp工程中,在每個需要傳遞引數至原生dll函式的結構體上,用[StructLayout(LayoutKind.Sequential)]進行標註,然後正常呼叫即可。

相關推薦

CSharpC/CPP技術簡要說明

CSharp與C/CPP混編 (mixing coding with CSharp and C/CPP)混合程式設計技術,是一種在工程上廣泛存在,並且已經發展了很多年的“古老”技術,常見的混合程式設計,有Java/C的程式設計,也有今天要提到的CSharp/C的組合形式。說到底,我們使用混合程式設計技術,是因

FortranC

動態鏈接 調用程序 運行 vc++ clu 出現一次 可執行 裏的 lock \(Fortran\) 作為用於科學計算的一種編譯型語言積累了大量數值計算的庫,但對於現代編程來說, \(Fortran\) 無 \(GUI\)庫 是其一大短板。本文就\(Fortran\) 與

iOS開發時OCC中,strcpy導致的記憶體溢位、野指標

在最近的專案開發中,由於需要使用C語言的演算法供給OC專案呼叫,所以研究了一下OC與C的混編及.a庫的相關生成。而在混編的過程中,C語言的演算法都能正常呼叫了,但是被一個問題困擾了很長一段時間,就是在

CPP檔案和C檔案和將sqlite3加入自己的c++工程

今天嘗試將使用sqlite3資料庫,直接使用sqlite3的原始碼,得到sqlite3.c和sqlite3.h。 我想將他們加入到我的cpp工程裡面 所以我新建了一個mysqlite3.cpp檔案,在裡面呼叫了sqlite3的函式。 下面來說明我遇到的問題及解決方法 一共有兩種編譯方

Python和C|C++的(二):利用Cython進行

cde uil 有時 當前 class def 將在 python 混編 還能夠使用Cython來實現混編 1 下載Cython。用python setup.py install進行安裝 2 一個實例 ① 創建helloworld文件夾創建hellowor

Python3 C# 並發程之~ 上篇

動態 ken summary using 任務調度 影響 特征 可能 arp NetCore並發編程 示例代碼:https://github.com/lotapp/BaseCode/tree/master/netcore/4_Concurrency 先簡單說下概念(其實之前

iOS開發之OCswift開發教程,代理的相互呼叫,block的實現。OC呼叫Swift中的代理, OC呼叫Swift中的Block 閉包

  本文章將從兩個方向分別介紹 OC 與 swift 混編   1. 第一個方向從 swift工程 中引入 oc類    1. 1 如何在swift的類中使用oc類    1.2  如何在swift中實現oc的代理方法  &

IOS 新建的Framework中OCswift的

OC與swift混編 在最上層專案中混編 swift引用swift swift引用OC OC引用swift OC引用OC 在Framework層內混編 swift引用swift swift

iOS C++/OC

先說題外話,文章標題其實起的不好,在iOS的開發中,Apple建立的庫基本都是用Objective-C寫的,所以在這裡的C++指的其實是Objective-C++。首先,最最最要緊的事情,不是程式碼而是編譯器選項,在做混合編譯之前一定要把編譯器的Compile Sources As選項改為Objective

iOS開發之OCswift開發教程,代理的相互呼叫,block的實現。OC呼叫Swift中的代理, OC呼叫Swift中的Block 閉包,swift 3.0

最新一些學妹問起,所以抽點時間來寫的,適合入門級別的swift 與 OC 混編 的程式猿。   本文章將從兩個方向分別介紹 OC 與 swift 混編   1. 第一個方向從 swift工程 中引入 oc類      1. 1 如何在swift的類中使用oc類     1

ARC 下 C++/OC 計數器的問題

ARC 模式下,object-C 編寫的程式碼的記憶體管理都交給了自動引用計數器了,不用我們自己再去操心記憶體的管理,但使用 Core Foundation 框架或者是和 C++ 混編時,程式碼的記憶體還是需要我們自己去管理的,這時候就需要注意了! Core

MATLAB/C語言的第一步,在MATLAB R2017b中生成mex檔案

Matlab在科學計算方面的優越性使其成為科研人員的必備軟體之一,搭載了大量複雜工具箱,日益優雅的程式碼編輯器讓MATLAB R2017b徹底替代了之前的先輩版本。然而MATLAB畢竟是一個商業化的數學軟體,在大型演算法的編寫和執行上都存在諸多缺點。因此一些大神們更喜歡用C

Python核心原始碼解析C/CPP-API拓展程式設計(一)PyObject

開發十年,就只剩下這套架構體系了! >>>   

MatalbC時矩陣無法傳到編譯好的mex文件

read www .html matalb tps 1-1 https mex文件 sca %E8%AF%B7%E9%97%AE%E6%88%91%E5%86%99%E7%9A%84%E8%BF%99%E4%B8%AA%E8%87%AA%E5%8A%A8%E7%94%9F%

CC++

clu 報錯 查看 工具 混編 define 別了 判斷 需要 了解一下C與C++如何合作,gcc和g++編譯出來的東西有什麽區別。 工具使用 objdump是個好工具,可以用於查看.o文件的內容,也可以查看可執行文件的內容。 查看符號表 objdump -t foo.o

OC C++ 導致的問題

object c cos alt idt tails blog fun 所有 src 最近項目中用到 std::function. 在導入頭文件的時候,發現問題總是報頭文件無法找到。但是我通過xcode 跳轉都能夠跳轉到對應的文件了 #include <strin

pythonC的執行邏輯

0x00 前言 之前遇到過一個C語言呼叫python的問題,是載入python版本可以在初始化前設定,如果不設定,會使用預設路徑/usr/bin/python. 設定方法為在 Py_Initialize呼叫前使用Py_SetProgramName,即可呼叫指定版本的python

IOS開發 C++遇到的問題

要解決這個問題,有二種辦法: 1)將XCode?工程中Build Setting -> Apple LLVM compiler Language 標籤下的C++ Language Dialect 和 C++ Standard Library 屬性值修改為 Co

c語言彙編寫法

R13 (stack pointer) R14 (Link Register) R15 (program counter) c語言中內嵌彙編的規則: 內嵌彙編器與armasm的區別: 訪問全域性變數: unsigned char              LDRB/

Objective-C Swift 之路

本文內容基於 Xcode 6.4 和 Swift 1.2 重要資料 為什麼要混編? 語言發展趨勢(TIOBE),Swift 排行持續上升, OC 排行呈重力下降 專案正常迭代需要 很多第三方庫仍然使用 OC 實現 專案中原來已經用