diff --git a/go/cmd/example.go b/go/cmd/example.go new file mode 100644 index 000000000..fa242f436 --- /dev/null +++ b/go/cmd/example.go @@ -0,0 +1,103 @@ +package main + +/** Example of using libvillas node-types in Go code + * + * This example demonstrate how you can use VILLASnode's + * node-types from a Go application. + */ + +import ( + "log" + "os" + "time" + + "git.rwth-aachen.de/acs/public/villas/node/go/pkg" + "git.rwth-aachen.de/acs/public/villas/node/go/pkg/config" + "git.rwth-aachen.de/acs/public/villas/node/go/pkg/node" + + "github.com/google/uuid" +) + +func main() { + cfg := &config.LoopbackNode{ + Node: config.Node{ + Name: "lo1", + Type: "loopback", + }, + In: config.NodeLoopbackIn{ + Hooks: []interface{}{ + &config.PrintHook{ + Hook: config.Hook{ + Type: "print", + }, + }, + }, + Signals: []config.Signal{ + { + Name: "sig1", + }, + { + Name: "sig2", + }, { + Name: "sig3", + }, + }, + }, + } + + n, err := node.NewNode(cfg, uuid.New()) + if err != nil { + log.Fatalf("Failed to create node: %s", err) + } + defer n.Close() + + if err := n.Check(); err != nil { + log.Fatalf("Failed to check node: %s", err) + } + + if err := n.Prepare(); err != nil { + log.Fatalf("Failed to prepare node: %s", err) + } + + if err := n.Start(); err != nil { + log.Fatalf("Failed to start node: %s", err) + } + defer n.Stop() + + log.Printf("%s\n", n.NameFull()) + + now := time.Now() + + smps_send := []node.Sample{ + { + Sequence: 1234, + Timestamps: pkg.Timestamps{ + Origin: pkg.Timestamp{now.Unix(), now.UnixNano()}, + }, + Data: []float64{1.1, 2.2, 3.3}, + }, + { + Sequence: 1235, + Timestamps: pkg.Timestamps{ + Origin: pkg.Timestamp{now.Unix(), now.UnixNano()}, + }, Data: []float64{4.4, 5.5, 6.6}, + }, + } + + log.Printf("Sent: %+#v\n", smps_send) + + cnt_written := n.Write(smps_send) + smps_received := n.Read(cnt_written) + + log.Printf("Received: %+#v\n", smps_received) + + if len(smps_send) != len(smps_received) { + log.Printf("Different length") + os.Exit(-1) + } + + if smps_send[0].Data[0] != smps_received[0].Data[0] { + log.Printf("Different data") + os.Exit(-1) + } +} diff --git a/go/pkg/node/node.go b/go/pkg/node/node.go new file mode 100644 index 000000000..f7b7c806a --- /dev/null +++ b/go/pkg/node/node.go @@ -0,0 +1,202 @@ +package node + +// #cgo LDFLAGS: -lvillas +// #include +import "C" +import ( + "encoding/json" + "fmt" + "log" + "unsafe" + + "github.com/google/uuid" + + "git.rwth-aachen.de/acs/public/villas/node/go/pkg" + "git.rwth-aachen.de/acs/public/villas/node/go/pkg/errors" +) + +const MAX_SIGNALS = 64 +const NUM_HUGEPAGES = 100 + +type Node struct { + inst *C.vnode +} + +func IsValidNodeNname(name string) bool { + return bool(C.node_is_valid_name(C.CString(name))) +} + +func NewNode(cfg interface{}, sn_uuid uuid.UUID) (*Node, error) { + if js, err := json.Marshal(cfg); err != nil { + return nil, fmt.Errorf("failed to serialize config: %w", err) + } else { + log.Printf("Config = %s\n", js) + n := C.node_new(C.CString(string(js)), C.CString(sn_uuid.String())) + return &Node{n}, nil + } +} + +func (n *Node) Prepare() error { + return errors.IntToError(int(C.node_prepare(n.inst))) +} + +func (n *Node) Check() error { + return errors.IntToError(int(C.node_check(n.inst))) +} + +func (n *Node) Start() error { + return errors.IntToError(int(C.node_start(n.inst))) +} + +func (n *Node) Stop() error { + return errors.IntToError(int(C.node_stop(n.inst))) +} + +func (n *Node) Pause() error { + return errors.IntToError(int(C.node_pause(n.inst))) +} + +func (n *Node) Resume() error { + return errors.IntToError(int(C.node_resume(n.inst))) +} + +func (n *Node) Restart() error { + return errors.IntToError(int(C.node_restart(n.inst))) +} + +func (n *Node) Close() error { + return errors.IntToError(int(C.node_destroy(n.inst))) +} + +func (n *Node) Reverse() error { + return errors.IntToError(int(C.node_reverse(n.inst))) +} + +func (n *Node) Read(cnt int) []Sample { + var csmps = make([]*C.vsample, cnt) + + for i := 0; i < cnt; i++ { + csmps[i] = C.sample_alloc(MAX_SIGNALS) + } + + read := int(C.node_read(n.inst, (**C.vsample)(unsafe.Pointer(&csmps[0])), C.uint(cnt))) + + var smps = make([]Sample, read) + for i := 0; i < read; i++ { + smps[i].FromC(csmps[i]) + C.sample_decref(csmps[i]) + } + + return smps +} + +func (n *Node) Write(smps []Sample) int { + cnt := len(smps) + var csmps = make([]*C.vsample, cnt) + + for i := 0; i < cnt; i++ { + csmps[i] = smps[i].ToC() + } + + return int(C.node_write(n.inst, (**C.vsample)(unsafe.Pointer(&csmps[0])), C.uint(cnt))) +} + +func (n *Node) PollFDs() []int { + var cfds [16]C.int + cnt := int(C.node_poll_fds(n.inst, (*C.int)(unsafe.Pointer(&cfds)))) + + var fds = make([]int, cnt) + for i := 0; i < cnt; i++ { + fds[i] = int(cfds[i]) + } + + return fds +} + +func (n *Node) NetemFDs() []int { + var cfds [16]C.int + cnt := int(C.node_netem_fds(n.inst, (*C.int)(unsafe.Pointer(&cfds)))) + + var fds = make([]int, cnt) + for i := 0; i < cnt; i++ { + fds[i] = int(cfds[i]) + } + + return fds +} + +func (n *Node) IsEnabled() bool { + return bool(C.node_is_enabled(n.inst)) +} + +func (n *Node) Name() string { + return C.GoString(C.node_name(n.inst)) +} + +func (n *Node) NameShort() string { + return C.GoString(C.node_name_short(n.inst)) +} + +func (n *Node) NameFull() string { + return C.GoString(C.node_name_full(n.inst)) +} + +func (n *Node) Details() string { + return C.GoString(C.node_details(n.inst)) +} + +func (n *Node) InputSignalsMaxCount() uint { + return uint(C.node_input_signals_max_cnt(n.inst)) +} + +func (n *Node) OutputSignalsMaxCount() uint { + return uint(C.node_output_signals_max_cnt(n.inst)) +} + +func (n *Node) ToJSON() string { + json_str := C.node_to_json_str(n.inst) + return C.GoString(json_str) +} + +func init() { + C.memory_init(NUM_HUGEPAGES) +} + +type Sample pkg.Sample + +func (s *Sample) ToC() *C.vsample { + return C.sample_pack( + C.uint(s.Sequence), + &C.struct_timespec{ + tv_sec: C.long(s.Timestamps.Origin[0]), + tv_nsec: C.long(s.Timestamps.Origin[1]), + }, + &C.struct_timespec{ + tv_sec: C.long(s.Timestamps.Received[0]), + tv_nsec: C.long(s.Timestamps.Received[1]), + }, + C.uint(len(s.Data)), + (*C.double)(unsafe.Pointer(&s.Data[0])), + ) +} + +func (s *Sample) FromC(c *C.vsample) { + var tsOrigin C.struct_timespec + var tsReceived C.struct_timespec + + len := C.sample_length(c) + + s.Data = make([]float64, uint(len)) + + C.sample_unpack(c, + (*C.uint)(unsafe.Pointer(&s.Sequence)), + &tsOrigin, + &tsReceived, + (*C.int)(unsafe.Pointer(&s.Flags)), + &len, + (*C.double)(unsafe.Pointer(&s.Data[0])), + ) + + s.Timestamps.Origin = pkg.Timestamp{int64(tsOrigin.tv_sec), int64(tsOrigin.tv_nsec)} + s.Timestamps.Received = pkg.Timestamp{int64(tsReceived.tv_sec), int64(tsReceived.tv_nsec)} +} diff --git a/go/cmd/test.go b/go/pkg/node/node_test.go similarity index 64% rename from go/cmd/test.go rename to go/pkg/node/node_test.go index 03036909c..166098397 100644 --- a/go/cmd/test.go +++ b/go/pkg/node/node_test.go @@ -1,17 +1,16 @@ -package main +package node_test import ( - "fmt" - "log" + "testing" "time" - node "git.rwth-aachen.de/acs/public/villas/node/go/pkg" "git.rwth-aachen.de/acs/public/villas/node/go/pkg/config" + "git.rwth-aachen.de/acs/public/villas/node/go/pkg/node" "github.com/google/uuid" ) -func main() { +func TestNode(t *testing.T) { cfg := &config.LoopbackNode{ Node: config.Node{ Name: "lo1", @@ -40,24 +39,24 @@ func main() { n, err := node.NewNode(cfg, uuid.New()) if err != nil { - log.Fatalf("Failed to create node: %s", err) + t.Fatalf("Failed to create node: %s", err) } defer n.Close() if err := n.Check(); err != nil { - log.Fatalf("Failed to check node: %s", err) + t.Fatalf("Failed to check node: %s", err) } if err := n.Prepare(); err != nil { - log.Fatalf("Failed to prepare node: %s", err) + t.Fatalf("Failed to prepare node: %s", err) } if err := n.Start(); err != nil { - log.Fatalf("Failed to start node: %s", err) + t.Fatalf("Failed to start node: %s", err) } defer n.Stop() - fmt.Printf("%s\n", n.NameFull()) + t.Logf("%s", n.NameFull()) smps_send := []node.Sample{ { @@ -72,10 +71,17 @@ func main() { }, } - fmt.Printf("Sent: %+#v\n", smps_send) + t.Logf("Sent: %+#v", smps_send) cnt_written := n.Write(smps_send) - smps_received := n.Read(cnt_written) + if cnt_written != len(smps_send) { + t.Fatalf("Failed to send all samples") + } - fmt.Printf("Received: %+#v\n", smps_received) + smps_received := n.Read(cnt_written) + if len(smps_received) != cnt_written { + t.Fatalf("Failed to receive samples back") + } + + t.Logf("Received: %+#v", smps_received) } diff --git a/go/pkg/sample.go b/go/pkg/sample.go index d2134d84d..e3fa60b6d 100644 --- a/go/pkg/sample.go +++ b/go/pkg/sample.go @@ -18,6 +18,7 @@ type Timestamps struct { } type Sample struct { + Flags int `json:"-"` Timestamps Timestamps `json:"ts"` Sequence uint64 `json:"sequence"` Data []float64 `json:"data"` @@ -43,3 +44,29 @@ func (s Sample) Bytes() []byte { b, _ := json.Marshal(s) return b } + +func (s Sample) Equal(t Sample) bool { + if s.Flags != t.Flags { + return false + } + + if s.Sequence != t.Sequence { + return false + } + + if s.Timestamps != t.Timestamps { + return false + } + + if len(s.Data) != len(t.Data) { + return false + } + + for i, va := range s.Data { + if vb := t.Data[i]; va != vb { + return false + } + } + + return true +} diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index 54542a9b0..bf7f0fecc 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -153,6 +153,11 @@ add_library(villas SHARED ${LIB_SRC}) target_include_directories(villas PUBLIC ${INCLUDE_DIRS}) target_link_libraries(villas PUBLIC ${LIBRARIES}) +# We need to link with -Bsymbolic in order to use link libvillas +# with Go code (Cgo) +# See also: https://stackoverflow.com/a/67299849 +target_link_options(villas PUBLIC "-Wl,-Bsymbolic") + if(APPLE) target_link_libraries(villas PRIVATE -Wl,-all_load ${WHOLE_ARCHIVES} -Wl,-noall_load) else()