Fungsi Panggilan Pergi dari C

150

Saya mencoba membuat objek statis yang ditulis dalam antarmuka Pergi dengan program C (katakanlah, modul kernel atau sesuatu).

Saya telah menemukan dokumentasi tentang pemanggilan fungsi C dari Go, tetapi saya belum menemukan banyak tentang bagaimana cara sebaliknya. Apa yang saya temukan adalah mungkin, tetapi rumit.

Inilah yang saya temukan:

Posting blog tentang panggilan balik antara C dan Go

Dokumentasi Cgo

Posting milis Golang

Adakah yang punya pengalaman dengan ini? Singkatnya, saya mencoba membuat modul PAM yang ditulis seluruhnya dalam Go.

hajar
sumber
11
Anda tidak bisa, setidaknya dari utas yang tidak dibuat dari Go. Saya sudah mengamuk tentang ini berkali-kali, dan berhenti berkembang di Go sampai ini diperbaiki.
Matt Joiner
Saya mendengar bahwa itu mungkin. Apakah tidak ada solusi?
beatgammit
Go menggunakan konvensi panggilan yang berbeda dan tumpukan tersegmentasi. Anda mungkin dapat menautkan kode Go yang dikompilasi dengan gccgo dengan kode C, tetapi saya belum mencoba ini karena saya belum mendapatkan gccgo untuk dibuat di sistem saya.
mkb
Saya sedang mencoba menggunakan SWIG sekarang, dan saya berharap ... Saya belum mendapatkan apa-apa untuk bekerja ... = '(Saya memposting di milis. Mudah-mudahan ada yang mengasihani saya.
beatgammit
2
Anda dapat memanggil kode Go dari C, tetapi saat ini Anda tidak dapat menanamkan runtime Go ke aplikasi C, yang merupakan perbedaan penting, namun halus.
tylerl

Jawaban:

126

Anda dapat memanggil kode Go dari C. itu adalah proposisi yang membingungkan.

Prosesnya diuraikan dalam posting blog yang Anda tautkan. Tapi saya bisa melihat bagaimana itu tidak terlalu membantu. Berikut ini cuplikan singkat tanpa bit yang tidak perlu. Seharusnya hal-hal sedikit lebih jelas.

package foo

// extern int goCallbackHandler(int, int);
//
// static int doAdd(int a, int b) {
//     return goCallbackHandler(a, b);
// }
import "C"

//export goCallbackHandler
func goCallbackHandler(a, b C.int) C.int {
    return a + b
}

// This is the public function, callable from outside this package.
// It forwards the parameters to C.doAdd(), which in turn forwards
// them back to goCallbackHandler(). This one performs the addition
// and yields the result.
func MyAdd(a, b int) int {
   return int( C.doAdd( C.int(a), C.int(b)) )
}

Urutan semua hal disebut adalah sebagai berikut:

foo.MyAdd(a, b) ->
  C.doAdd(a, b) ->
    C.goCallbackHandler(a, b) ->
      foo.goCallbackHandler(a, b)

Kunci untuk diingat di sini adalah bahwa fungsi panggilan balik harus ditandai dengan //exportkomentar di sisi Go dan seperti externdi sisi C. Ini berarti bahwa setiap panggilan balik yang ingin Anda gunakan, harus ditentukan di dalam paket Anda.

Untuk memungkinkan pengguna paket Anda untuk menyediakan fungsi panggilan balik khusus, kami menggunakan pendekatan yang sama persis seperti di atas, tetapi kami menyediakan penangan kustom pengguna (yang hanya merupakan fungsi Go biasa) sebagai parameter yang diteruskan ke C sisi sebagai void*. Ini kemudian diterima oleh penelepon balik dalam paket kami dan dipanggil.

Mari kita gunakan contoh yang lebih maju yang saat ini saya kerjakan. Dalam hal ini, kami memiliki fungsi C yang melakukan tugas yang cukup berat: Ini membaca daftar file dari perangkat USB. Ini bisa memakan waktu cukup lama, jadi kami ingin aplikasi kami diberitahu tentang perkembangannya. Kita dapat melakukan ini dengan mengirimkan pointer fungsi yang kita tetapkan dalam program kita. Ini hanya menampilkan beberapa info kemajuan kepada pengguna setiap kali dipanggil. Karena memiliki tanda tangan yang terkenal, kita dapat menetapkan jenisnya sendiri:

type ProgressHandler func(current, total uint64, userdata interface{}) int

Handler ini mengambil beberapa info kemajuan (jumlah file yang diterima saat ini dan jumlah total file) bersama dengan nilai antarmuka {} yang dapat menampung apa saja yang dibutuhkan pengguna untuk dipegang.

Sekarang kita perlu menulis pipa C dan Go untuk memungkinkan kita menggunakan penangan ini. Untungnya fungsi C yang ingin saya panggil dari pustaka memungkinkan kita untuk memasukkan data tipe userdata void*. Ini berarti dapat menyimpan apa pun yang kita inginkan, tidak ada pertanyaan yang diajukan dan kita akan mendapatkannya kembali ke Dunia Go apa adanya. Untuk membuat semua ini berfungsi, kami tidak memanggil fungsi pustaka dari Go secara langsung, tetapi kami membuat pembungkus C untuk itu yang akan kami beri nama goGetFiles(). Ini pembungkus ini yang benar-benar memasok panggilan balik Go kami ke perpustakaan C, bersama dengan objek data pengguna.

package foo

// #include <somelib.h>
// extern int goProgressCB(uint64_t current, uint64_t total, void* userdata);
// 
// static int goGetFiles(some_t* handle, void* userdata) {
//    return somelib_get_files(handle, goProgressCB, userdata);
// }
import "C"
import "unsafe"

Perhatikan bahwa goGetFiles()fungsi tidak mengambil pointer fungsi untuk panggilan balik sebagai parameter. Alih-alih, panggilan balik yang diberikan pengguna kami dikemas dalam struct khusus yang berisi nilai data pengguna itu sendiri. Kami meneruskan ini goGetFiles()sebagai parameter data pengguna.

// This defines the signature of our user's progress handler,
type ProgressHandler func(current, total uint64, userdata interface{}) int 

// This is an internal type which will pack the users callback function and userdata.
// It is an instance of this type that we will actually be sending to the C code.
type progressRequest struct {
   f ProgressHandler  // The user's function pointer
   d interface{}      // The user's userdata.
}

//export goProgressCB
func goProgressCB(current, total C.uint64_t, userdata unsafe.Pointer) C.int {
    // This is the function called from the C world by our expensive 
    // C.somelib_get_files() function. The userdata value contains an instance
    // of *progressRequest, We unpack it and use it's values to call the
    // actual function that our user supplied.
    req := (*progressRequest)(userdata)

    // Call req.f with our parameters and the user's own userdata value.
    return C.int( req.f( uint64(current), uint64(total), req.d ) )
}

// This is our public function, which is called by the user and
// takes a handle to something our C lib needs, a function pointer
// and optionally some user defined data structure. Whatever it may be.
func GetFiles(h *Handle, pf ProgressFunc, userdata interface{}) int {
   // Instead of calling the external C library directly, we call our C wrapper.
   // We pass it the handle and an instance of progressRequest.

   req := unsafe.Pointer(&progressequest{ pf, userdata })
   return int(C.goGetFiles( (*C.some_t)(h), req ))
}

Itu saja untuk binding C kami. Kode pengguna sekarang sangat mudah:

package main

import (
    "foo"
    "fmt"
)

func main() {
    handle := SomeInitStuff()

    // We call GetFiles. Pass it our progress handler and some
    // arbitrary userdata (could just as well be nil).
    ret := foo.GetFiles( handle, myProgress, "Callbacks rock!" )

    ....
}

// This is our progress handler. Do something useful like display.
// progress percentage.
func myProgress(current, total uint64, userdata interface{}) int {
    fc := float64(current)
    ft := float64(total) * 0.01

    // print how far along we are.
    // eg: 500 / 1000 (50.00%)
    // For good measure, prefix it with our userdata value, which
    // we supplied as "Callbacks rock!".
    fmt.Printf("%s: %d / %d (%3.2f%%)\n", userdata.(string), current, total, fc / ft)
    return 0
}

Ini semua terlihat jauh lebih rumit daripada itu. Pesanan panggilan tidak berubah dibandingkan dengan contoh kami sebelumnya, tetapi kami mendapatkan dua panggilan tambahan di akhir rantai:

Urutannya adalah sebagai berikut:

foo.GetFiles(....) ->
  C.goGetFiles(...) ->
    C.somelib_get_files(..) ->
      C.goProgressCB(...) ->
        foo.goProgressCB(...) ->
           main.myProgress(...)
Jim
sumber
Ya, saya menyadari bahwa semua ini mungkin meledak di wajah kita ketika utas terpisah ikut bermain. Khususnya yang tidak dibuat oleh Go. Sayangnya itulah yang terjadi pada saat ini.
hanya
17
Ini jawaban yang sangat bagus, dan teliti. Itu tidak langsung menjawab pertanyaan, tetapi itu karena tidak ada jawaban. Menurut beberapa sumber, titik masuknya harus Go, dan tidak boleh C. Saya menandai ini sebagai benar karena ini benar-benar menjelaskan bagi saya. Terima kasih!
beatgammit
@jimt Bagaimana cara mengintegrasikan dengan pengumpul sampah? Secara khusus, kapan instance progressRequest pribadi dikumpulkan, jika pernah? (Baru untuk Pergi, dan dengan demikian ke unsafe.Pointer). Juga, bagaimana dengan API seperti SQLite3 yang mengambil void * userdata, tetapi juga fungsi "deleter" opsional untuk data pengguna? Bisakah itu digunakan untuk berinteraksi dengan GC, untuk mengatakan "sekarang tidak apa-apa untuk mengklaim kembali data pengguna itu, asalkan pihak Go tidak lagi merujuknya?".
ddevienne
6
Pada Go 1.5 ada dukungan yang lebih baik untuk memanggil Go from C. Lihat pertanyaan ini jika Anda mencari jawaban yang menunjukkan teknik yang lebih sederhana: stackoverflow.com/questions/32215509/…
Gabriel Southern
2
Pada Go 1.6, pendekatan ini tidak berfungsi, itu memecahkan "kode C mungkin tidak menyimpan salinan pointer Go setelah panggilan kembali." aturan dan mengeluarkan kesalahan "panik: runtime: argumen cgo memiliki Go pointer to Go pointer" saat runtime
kaspersky
56

Itu bukan proposisi yang membingungkan jika Anda menggunakan gccgo. Ini berfungsi di sini:

foo.go

package main

func Add(a, b int) int {
    return a + b
}

bar.c

#include <stdio.h>

extern int go_add(int, int) __asm__ ("example.main.Add");

int main() {
  int x = go_add(2, 3);
  printf("Result: %d\n", x);
}

Makefile

all: main

main: foo.o bar.c
    gcc foo.o bar.c -o main

foo.o: foo.go
    gccgo -c foo.go -o foo.o -fgo-prefix=example

clean:
    rm -f main *.o
Alexander
sumber
ketika saya sudah pergi kode lakukan sth dengan string go package main func Add(a, b string) int { return a + b }saya mendapatkan kesalahan "undefined _go_string_plus"
TruongSinh
2
TruongSinh, Anda mungkin ingin menggunakan cgodan gobukan gccgo. Lihat golang.org/cmd/cgo . Ketika itu dikatakan, sangat mungkin untuk menggunakan tipe "string" dalam file .go dan mengubah file .c Anda untuk memasukkan __go_string_plusfungsi. Ini berfungsi: ix.io/dZB
Alexander
3

Sejauh yang saya ketahui tidak mungkin:

Catatan: Anda tidak dapat menentukan fungsi C apa pun dalam pembukaan jika Anda menggunakan ekspor.

sumber: https://github.com/golang/go/wiki/cgo

Rafael Ribeiro
sumber