いろいろ備忘録日記

主に .NET とか Go とか Flutter とか Python絡みのメモを公開しています。

Goメモ-526 (cgoメモ-17)(cgoとdlopen関数を使って既存ライブラリの呼び出しをフックする)

関連記事

Goメモ-502 (cgoメモ-01)(cgoヘッダ) - いろいろ備忘録日記

Goメモ-506 (cgoメモ-02)(cgoヘッダ) - いろいろ備忘録日記

Goメモ-507 (cgoメモ-03)(C.int) - いろいろ備忘録日記

Goメモ-508 (cgoメモ-04)(C言語の構造体) - いろいろ備忘録日記

Goメモ-509 (cgoメモ-05)(C.CString)(Cの文字列) - いろいろ備忘録日記

Goメモ-510 (cgoメモ-06)(C.GoString)(Cの文字列をGoの文字列へ) - いろいろ備忘録日記

Goメモ-511 (cgoメモ-07)(C.CBytes)([]byteをCのバイト列に) - いろいろ備忘録日記

Goメモ-512 (cgoメモ-08)(C.GoBytes)(Cのバイト列をGoの[]byteへ) - いろいろ備忘録日記

Goメモ-514 (cgoメモ-09)(C.GoStringN)(C.GoStringのサイズ指定版) - いろいろ備忘録日記

Goメモ-515 (cgoメモ-10)([]byteを(void *)へ変換) - いろいろ備忘録日記

Goメモ-516 (cgoメモ-11)([]byteを(char *)へ変換) - いろいろ備忘録日記

Goメモ-518 (cgoメモ-12)(Cのmallocをcgo経由で呼び出し) - いろいろ備忘録日記

Goメモ-519 (cgoメモ-13)(ポインタ演算) - いろいろ備忘録日記

Goメモ-520 (cgoメモ-14)(Goの関数をCの世界に公開 (export)) - いろいろ備忘録日記

Goメモ-522 (cgoメモ-15)(Goでsoファイルを作成してC言語から呼び出し) - いろいろ備忘録日記

Goメモ-525 (cgoメモ-16)(C側にて関数ポインタを引数に要求する関数にGo側で定義した関数を設定) - いろいろ備忘録日記

GitHub - devlights/blog-summary: ブログ「いろいろ備忘録日記」のまとめ

概要

以下、自分用のメモです。

今回から複数回に渡って cgo についてメモしていこうと思います。

cgo は、文字通りGoからCにアクセスすることが出来るようになるものなのですが、とても便利な反面、結構クセが強いのでメモでも残しておかないとすぐ頭から消えてしまいそうだなって思いました。

Cgo is not Go

という格言があったりするので、Go界隈で標準で推奨されていない技術かもしれません。が、実務ではC言語で作成されたライブラリなどは山のようにあります。んで、プロジェクトの方針でGoで作り直すことも出来ない場合も多々あります。そのような場合に非常に便利です。

これからのサンプルは以下のリポジトリにアップしてありますので、良ければご参考ください。

github.com

今回は cgoとdlopen関数を使って、既に存在している共有ライブラリ内の既存関数と全く同じ関数書式を持つ関数をcgo側で定義して、呼び出しをフックしてみます。

dlopen関数って何?って方は、以下を参照くださいませ。

manpages.ubuntu.com

フックするときによく利用されますね。

今回のサンプルはちょっとソースファイル数が多いので、完全版のサンプルをご覧になりたい場合は try-golang-cgo 16.C_dlopen_dlsym を参照くださいませ。

サンプル

以下のサンプルは、予め

size_t my_strlen(const char *s);

という関数が定義された soファイル があり (libclib.so) 、cgo側で my_strlen 関数をフックして追加の挙動を組み込むというサンプルになります。

dlopen.go

package dlopen

/*
#cgo LDFLAGS: -ldl

#include <dlfcn.h>
#include <stdlib.h>
*/
import "C"
import (
    "unsafe"
)

func OpenLib(path string) unsafe.Pointer {
    var (
        cPath    = C.CString(path)
        cPathPtr = unsafe.Pointer(cPath)
        handle   unsafe.Pointer
    )
    defer C.free(cPathPtr)

    handle = C.dlopen(cPath, C.RTLD_LAZY)
    return handle
}

func GetSym(handle unsafe.Pointer, symbol string) unsafe.Pointer {
    var (
        cSymbol    = C.CString(symbol)
        cSymbolPtr = unsafe.Pointer(cSymbol)
        funcPtr    unsafe.Pointer
    )
    defer C.free(cSymbolPtr)

    // 本来、dlsym()を呼び出す前に dlerror() を呼び出し、エラー状態をクリアしてから
    // dlsym()を呼び出す。その後に、dlerror() でエラーを確認するのが正当な流れであるが割愛

    funcPtr = C.dlsym(handle, cSymbol)
    return funcPtr
}

func CloseLib(handle unsafe.Pointer) {
    // 本来、dlclose()を呼び出す前に dlerror() を呼び出し、エラー状態をクリアしてから
    // dlclose()を呼び出す。その後に、dlerror() でエラーを確認するのが正当な流れであるが割愛
    C.dlclose(handle)
}

gofuncs.go

package main

/*
extern size_t call_my_strlen(void *fn, const char *s);
*/
import "C"
import (
    "log"
    "unsafe"

    "github.com/devlights/try-golang-cgo/16.C_dlopen_dlsym/dlopen"
)

//export my_strlen
func my_strlen(cStr *C.char) C.size_t {
    var (
        handle unsafe.Pointer
        symbol unsafe.Pointer
        result C.size_t
    )

    handle = dlopen.OpenLib("clib/libclib.so")
    defer dlopen.CloseLib(handle)

    symbol = dlopen.GetSym(handle, "my_strlen")
    result = C.call_my_strlen(symbol, cStr)

    log.Printf("[Go][インターセプト][my_strlen] result=%v", result)

    return result
}

cfuncs.go

package main

/*
size_t call_my_strlen(void *fn, const char *s) {
   size_t (*my_strlen_fn)(const char *);

   my_strlen_fn = (size_t (*)(const char *))fn;
   return my_strlen_fn(s);
}
*/
import "C"

main.c

#include <stdio.h>
#include "clib/lib.h"

int main() {
    printf("[ C] my_strlen=%zu\n", my_strlen("helloworld"));
}

Taskfile.yml

# https://taskfile.dev

version: '3'

vars:
  CAPP_NAME: capp

tasks:
  default:
    cmds:
      - task: build-clib
      - task: build-golib
      - task: build-cprg
      - LD_LIBRARY_PATH=. ./{{.CAPP_NAME}}
      - task: build-cprg-original
      - LD_LIBRARY_PATH=clib ./{{.CAPP_NAME}}
  build-clib:
    dir: clib
    cmds:
      - gcc -fPIC -shared -o libclib.so lib.c
  build-golib:
    cmds:
      - go build -o libgolib.so -buildmode=c-shared *.go
  build-cprg:
    cmds:
      - gcc -c -o main.o main.c
      - gcc -o {{.CAPP_NAME}} main.o -L. -Lclib -lgolib -lclib
  build-cprg-original:
    cmds:
      - gcc -o {{.CAPP_NAME}} main.o -L. -Lclib -lclib

実行

$ task
task: [build-clib] gcc -fPIC -shared -o libclib.so lib.c
task: [build-golib] go build -o libgolib.so -buildmode=c-shared *.go
task: [build-cprg] gcc -c -o main.o main.c
task: [build-cprg] gcc -o capp main.o -L. -Lclib -lgolib -lclib
task: [default] LD_LIBRARY_PATH=. ./capp
[Go][インターセプト][my_strlen] result=10
[ C] my_strlen=10
task: [build-cprg-original] gcc -o capp main.o -L. -Lclib -lclib
task: [default] LD_LIBRARY_PATH=clib ./capp
[ C] my_strlen=10

ちゃんとインターセプトして元関数をよびつつログも出力していますね。

参考情報

Goのおすすめ書籍


過去の記事については、以下のページからご参照下さい。

サンプルコードは、以下の場所で公開しています。