diff --git a/jq.go b/jq.go index d7d0b4c..1fb9a00 100644 --- a/jq.go +++ b/jq.go @@ -1,11 +1,24 @@ package libjq_go import ( + "sync" + "github.com/flant/libjq-go/pkg/jq" ) -// Jq is handy shortcut to use a default jq invoker with enabled cache for programs +var initOnce sync.Once +var defaultCgoCaller jq.CgoCaller +var defaultCache *jq.JqCache + +// Jq is a handy shortcut to create a jq invoker with default settings. +// +// Created invokers will share a cache for programs and a cgo caller. func Jq() *jq.Jq { + initOnce.Do(func() { + defaultCache = jq.NewJqCache() + defaultCgoCaller = jq.NewCgoCaller() + }) return jq.NewJq(). - WithCache(jq.JqDefaultCache()) + WithCache(defaultCache). + WithCgoCaller(defaultCgoCaller) } diff --git a/jq_test.go b/jq_test.go index 81d2d8b..82a4c88 100644 --- a/jq_test.go +++ b/jq_test.go @@ -2,9 +2,12 @@ package libjq_go import ( "fmt" + "sync" "testing" . "github.com/onsi/gomega" + + "github.com/flant/libjq-go/pkg/jq" ) func Test_OneProgram_OneInput(t *testing.T) { @@ -66,3 +69,124 @@ func Test_RunError(t *testing.T) { g.Expect(err).Should(HaveOccurred()) g.Expect(err.Error()).To(ContainSubstring("Cannot iterate over string")) } + +// PoC of multiple jq thread +func Test_two_cgo_callers(t *testing.T) { + g := NewWithT(t) + + cgoCaller := jq.NewCgoCaller() + invoker := jq.NewJq(). + WithCache(jq.NewJqCache()). + WithCgoCaller(cgoCaller) + // New cache is required for every new cgoCaller! + // Cache saves jq_state from jq_compile in a pinned thread, so another cgoCaller + // cannot access that jq_state in another thread. + //WithCache(jq.JqDefaultCache()): + // Assertion failed: (0 && "invalid instruction"), function jq_nextAssertion failed: (jv_is_valid(v, file src/execute.c, line 401. + // al)), function stack_pop, file src/execute.c, line 177. + // SIGABRT: abort + + var wg sync.WaitGroup + + wg.Add(2) + + // jq with default cgo caller + go func() { + defer wg.Done() + + p1, _ := Jq().Program(`.bb//"NO"`).Precompile() + + for i := 0; i < 500; i++ { + res, err := Jq().Program(".foo").Run(`{"foo":"bar"}`) + g.Expect(err).ShouldNot(HaveOccurred()) + g.Expect(res).To(Equal(`"bar"`)) + + res, err = p1.Run(`{"foo":"bar"}`) + g.Expect(err).ShouldNot(HaveOccurred()) + g.Expect(res).To(Equal(`"NO"`)) + } + + }() + + // jq with another, parallel cgo caller + go func() { + defer wg.Done() + + p1, _ := invoker.Program(`.bb//"NO"`).Precompile() + + for i := 0; i < 500; i++ { + res, err := p1.Run(`{"foo":"bar"}`) + g.Expect(err).ShouldNot(HaveOccurred()) + g.Expect(res).To(Equal(`"NO"`)) + + res, err = invoker.Program(".foo").Run(`{"foo":"bar"}`) + g.Expect(err).ShouldNot(HaveOccurred()) + g.Expect(res).To(Equal(`"bar"`)) + } + + }() + + wg.Wait() +} + +// PoC of multiple jq thread +func Test_multiple_cgo_callers(t *testing.T) { + g := NewWithT(t) + + jqPoolLen := 16 + + // init invokers + jqPool := []*jq.Jq{} + for i := 0; i < jqPoolLen; i++ { + invoker := jq.NewJq(). + WithCache(jq.NewJqCache()). + WithCgoCaller(jq.NewCgoCaller()) + jqPool = append(jqPool, invoker) + } + + consumersCount := jqPoolLen * 2 // twice as invokers + + // Start consumers + var wg sync.WaitGroup + wg.Add(consumersCount) + + for i := 0; i < consumersCount; i++ { + invokerIndex := i % jqPoolLen + go func(n int) { + defer wg.Done() + + invoker := jqPool[n] + + p1, _ := invoker.Program(`.bb//"NO"`).Precompile() + + foo, _ := invoker.Program(".foo").Precompile() + + for i := 0; i < 100; i++ { + res, err := p1.Run(`{"foo":"bar"}`) + g.Expect(err).ShouldNot(HaveOccurred()) + g.Expect(res).To(Equal(`"NO"`)) + + res, err = foo.Run(`{"foo":"bar"}`) + g.Expect(err).ShouldNot(HaveOccurred()) + g.Expect(res).To(Equal(`"bar"`)) + } + + p2, _ := invoker.Program(`.bb//"YES"`).Precompile() + + p3, _ := invoker.Program(".foobar").Precompile() + + for i := 0; i < 100; i++ { + res, err := p2.Run(`{"foo":"bar"}`) + g.Expect(err).ShouldNot(HaveOccurred()) + g.Expect(res).To(Equal(`"YES"`)) + + res, err = p3.Run(`{"foobar":"bar"}`) + g.Expect(err).ShouldNot(HaveOccurred()) + g.Expect(res).To(Equal(`"bar"`)) + } + + }(invokerIndex) + } + + wg.Wait() +} diff --git a/pkg/jq/cache.go b/pkg/jq/cache.go index 936a9be..5aec73c 100644 --- a/pkg/jq/cache.go +++ b/pkg/jq/cache.go @@ -6,24 +6,12 @@ import ( "github.com/flant/libjq-go/pkg/libjq" ) -/* -Simple cache for jq state objects with compiled programs. -*/ - +// JqCache is a simple cache for JqState objects. type JqCache struct { StateCache map[string]*libjq.JqState m sync.Mutex } -var jqDefaultCacheInstance *JqCache - -var JqDefaultCache = func() *JqCache { - if jqDefaultCacheInstance == nil { - jqDefaultCacheInstance = NewJqCache() - } - return jqDefaultCacheInstance -} - func NewJqCache() *JqCache { return &JqCache{ StateCache: make(map[string]*libjq.JqState), @@ -31,6 +19,7 @@ func NewJqCache() *JqCache { } } +// Get returns cached JqState object or nil of no object is registered for key. func (jc *JqCache) Get(key string) *libjq.JqState { jc.m.Lock() defer jc.m.Unlock() @@ -40,20 +29,23 @@ func (jc *JqCache) Get(key string) *libjq.JqState { return nil } +// Set register a JqState object for key. func (jc *JqCache) Set(key string, state *libjq.JqState) { jc.m.Lock() jc.StateCache[key] = state jc.m.Unlock() } +// Teardown calls Teardown for cached JqState object. func (jc *JqCache) Teardown(key string) { jc.m.Lock() defer jc.m.Unlock() - if v, ok := jc.StateCache[key]; ok { - v.Teardown() + if jqState, ok := jc.StateCache[key]; ok { + jqState.Teardown() } } +// TeardownAll calls Teardown for all cached JqState objects. func (jc *JqCache) TeardownAll() { jc.m.Lock() defer jc.m.Unlock() diff --git a/pkg/jq/cache_test.go b/pkg/jq/cache_test.go new file mode 100644 index 0000000..40c7990 --- /dev/null +++ b/pkg/jq/cache_test.go @@ -0,0 +1,74 @@ +package jq + +import ( + "sync" + "testing" + + . "github.com/onsi/gomega" + + "github.com/flant/libjq-go/pkg/libjq" +) + +func Test_JqCache_Get_Set(t *testing.T) { + g := NewWithT(t) + + s := &libjq.JqState{} + + c := NewJqCache() + c.Set("state", s) + + s2 := c.Get("state") + g.Expect(s2).Should(Equal(s)) +} + +func Test_JqCache_Get_Set_Parallel(t *testing.T) { + g := NewWithT(t) + + s1 := &libjq.JqState{} + s2 := &libjq.JqState{} + s3 := &libjq.JqState{} + + c := NewJqCache() + c.Set("state1", s1) + c.Set("state2", s2) + c.Set("state3", s3) + + var wg sync.WaitGroup + wg.Add(30) + + for i := 0; i < 10; i++ { + go func() { + defer wg.Done() + for i := 0; i < 10; i++ { + c.Set("state1", s1) + c.Set("state2", s2) + s := c.Get("state3") + g.Expect(s).Should(Equal(s3)) + } + }() + go func() { + defer wg.Done() + for i := 0; i < 10; i++ { + c.Set("state1", s1) + c.Set("state3", s3) + s := c.Get("state2") + g.Expect(s).Should(Equal(s2)) + } + }() + go func() { + defer wg.Done() + for i := 0; i < 10; i++ { + c.Set("state3", s3) + c.Set("state2", s2) + s := c.Get("state1") + g.Expect(s).Should(Equal(s1)) + } + }() + } + + wg.Wait() + + g.Expect(c.Get("state1")).Should(Equal(s1)) + g.Expect(c.Get("state2")).Should(Equal(s2)) + g.Expect(c.Get("state3")).Should(Equal(s3)) +} diff --git a/pkg/jq/cgo_caller.go b/pkg/jq/cgo_caller.go index ddee015..be984aa 100644 --- a/pkg/jq/cgo_caller.go +++ b/pkg/jq/cgo_caller.go @@ -6,33 +6,40 @@ import ( ) /* -libjq methods should run in one thread, so this trick with LockOsThread come up. +jq_state is thread safe, but not compatible with migration of go routines between thread. +That is why libjq methods should be called from the same thread where jq_state was created. +To achieve this, a dedicated go routine and a chan func() are used. */ -var cgoCallsCh chan func() -var mu = sync.Mutex{} +type CgoCaller func(func()) -// CgoCall is used to run C code of a jq in a dedicated go-routine locked to OS thread. -func CgoCall(f func()) { - mu.Lock() - if cgoCallsCh == nil { - cgoCallsCh = make(chan func()) - go func() { - runtime.LockOSThread() - for { - select { - case f := <-cgoCallsCh: - f() +// NewCgoCaller is a factory of CgoCallers. CgoCaller is a way to run C code of a jq in a dedicated go-routine locked to OS thread. +// CgoCaller on first invoke creates a channel and starts a go-routine locked to os thread. This go-routine receives tasks to run via a channel. + +func NewCgoCaller() CgoCaller { + var cgoCallTasksCh chan func() + var initOnce sync.Once + + return func(f func()) { + initOnce.Do(func() { + cgoCallTasksCh = make(chan func()) + go func() { + runtime.LockOSThread() + for { + select { + case f := <-cgoCallTasksCh: + f() + } } - } - }() - } - mu.Unlock() + }() + }) - done := make(chan struct{}, 1) - cgoCallsCh <- func() { - f() - done <- struct{}{} + var wg sync.WaitGroup + wg.Add(1) + cgoCallTasksCh <- func() { + f() + wg.Done() + } + wg.Wait() } - <-done } diff --git a/pkg/jq/cgo_caller_test.go b/pkg/jq/cgo_caller_test.go index 6c03fcc..b608499 100644 --- a/pkg/jq/cgo_caller_test.go +++ b/pkg/jq/cgo_caller_test.go @@ -1,6 +1,7 @@ package jq import ( + "github.com/flant/libjq-go/pkg/libjq" "testing" . "github.com/onsi/gomega" @@ -9,19 +10,37 @@ import ( func Test_CgoCall(t *testing.T) { g := NewWithT(t) - in := `{"foo":"baz","bar":"quux"}` + testProgram := `.foo` + testData := `{"foo": "bar"}` + testExpected := `"bar"` + testResult := "" - res, err := NewJq().Program(".").Run(in) - g.Expect(err).ShouldNot(HaveOccurred()) - g.Expect(res).To(Equal(in)) + var jqState *libjq.JqState + var err error + + caller := NewCgoCaller() - g.Expect(cgoCallsCh).ToNot(BeNil(), "cgo calls channel should not be nil after first run") + // Create jq state in locked OS thread memory. + caller(func() { + jqState, err = libjq.NewJqState() + }) + g.Expect(err).ShouldNot(HaveOccurred()) + g.Expect(jqState).ShouldNot(BeNil()) - res, err = NewJq().Program(".").Run(in) + // Compile program using created state. + caller(func() { + err = jqState.Compile(testProgram) + }) g.Expect(err).ShouldNot(HaveOccurred()) - g.Expect(res).To(Equal(in)) + g.Expect(jqState).ShouldNot(BeNil()) - res, err = NewJq().Program(".").Run(in) + // Process data with compiled program. + caller(func() { + defer jqState.Teardown() + testResult, err = jqState.ProcessOneValue(testData, false) + }) g.Expect(err).ShouldNot(HaveOccurred()) - g.Expect(res).To(Equal(in)) + g.Expect(jqState).ShouldNot(BeNil()) + + g.Expect(testResult).Should(Equal(testExpected)) } diff --git a/pkg/jq/jq.go b/pkg/jq/jq.go index 1a04e89..20996a2 100644 --- a/pkg/jq/jq.go +++ b/pkg/jq/jq.go @@ -7,13 +7,15 @@ import ( /* High level API for libjq. -Jq has this options: +Executor for jq programs has this options: - cache - library path +- cgo caller function to pin jq calls to an OS thread */ type Jq struct { - Cache *JqCache - LibPath string + Cache *JqCache + LibPath string + CgoCaller CgoCaller } func NewJq() *Jq { @@ -25,6 +27,11 @@ func (jq *Jq) WithCache(cache *JqCache) *Jq { return jq } +func (jq *Jq) WithCgoCaller(cgoCaller CgoCaller) *Jq { + jq.CgoCaller = cgoCaller + return jq +} + func (jq *Jq) WithLibPath(path string) *Jq { jq.LibPath = path return jq @@ -60,33 +67,31 @@ func (jqp *JqProgram) Precompile() (p *JqProgram, err error) { jqp.CacheLookup = true - CgoCall(func() { - _, err = jqp.compile() - }) + _, err = jqp.compile() + return jqp, err +} +// Compile compiles a program immediately, it returns error in case of syntax error. +func (jqp *JqProgram) Compile() (p *JqProgram, err error) { + _, err = jqp.compile() return jqp, err } // Run actually runs a program over passed data. It compiles program // if the program is not compiled yet. +// Returns an quoted string if filter result is a string. func (jqp *JqProgram) Run(data string) (s string, e error) { - CgoCall(func() { - s, e = jqp.run(data, false) - }) - return + return jqp.run(data, false) } // RunRaw actually runs a program over passed data. It compiles program // if the program is not compiled yet. // Returns an unquoted string if filter result is a string. func (jqp *JqProgram) RunRaw(data string) (s string, e error) { - CgoCall(func() { - s, e = jqp.run(data, true) - }) - return + return jqp.run(data, true) } -// compile create a jq state with compiled program and stores it in cache if needed. +// compile creates a new jq state with compiled program or just returns a cached one. func (jqp *JqProgram) compile() (state *libjq.JqState, err error) { if jqp.CacheLookup { inCacheState := jqp.Jq.Cache.Get(jqp.Program) @@ -94,15 +99,19 @@ func (jqp *JqProgram) compile() (state *libjq.JqState, err error) { return inCacheState, nil } } - state, err = libjq.NewJqState() - if err != nil { - return nil, err - } - state.SetLibraryPath(jqp.Jq.LibPath) - err = state.Compile(jqp.Program) + + jqp.Jq.CgoCaller(func() { + state, err = libjq.NewJqState() + if err != nil { + return + } + state.SetLibraryPath(jqp.Jq.LibPath) + err = state.Compile(jqp.Program) + }) if err != nil { - return nil, err + return } + if jqp.CacheLookup { jqp.Jq.Cache.Set(jqp.Program, state) } @@ -116,9 +125,13 @@ func (jqp *JqProgram) run(inJson string, rawMode bool) (res string, err error) { if err != nil { return "", err } - if !jqp.CacheLookup { - defer state.Teardown() - } - return state.ProcessOneValue(inJson, rawMode) + jqp.Jq.CgoCaller(func() { + res, err = state.ProcessOneValue(inJson, rawMode) + if !jqp.CacheLookup { + state.Teardown() + } + }) + + return } diff --git a/pkg/jq/jq_test.go b/pkg/jq/jq_test.go index bc613d0..533b1db 100644 --- a/pkg/jq/jq_test.go +++ b/pkg/jq/jq_test.go @@ -11,10 +11,24 @@ import ( . "github.com/onsi/gomega" ) +func newJq() (*Jq, *JqCache, CgoCaller) { + caller := NewCgoCaller() + cache := NewJqCache() + return NewJq().WithCache(cache).WithCgoCaller(caller), cache, caller +} + +func newSimpleJq() *Jq { + caller := NewCgoCaller() + cache := NewJqCache() + return NewJq().WithCache(cache).WithCgoCaller(caller) +} + func Test_FieldAccess(t *testing.T) { g := NewWithT(t) - res, err := NewJq().Program(".foo").Run(`{"foo":"baz"}`) + testJq := newSimpleJq() + + res, err := testJq.Program(".foo").Run(`{"foo":"baz"}`) g.Expect(err).ShouldNot(HaveOccurred()) g.Expect(res).To(Equal(`"baz"`)) } @@ -22,7 +36,7 @@ func Test_FieldAccess(t *testing.T) { func Test_JsonOutput(t *testing.T) { g := NewWithT(t) in := `{"foo":"baz","bar":"quux"}` - res, err := NewJq().Program(".").Run(in) + res, err := newSimpleJq().Program(".").Run(in) g.Expect(err).ShouldNot(HaveOccurred()) g.Expect(res).To(Equal(in)) } @@ -34,16 +48,40 @@ func Test_LibPath_FilteredFieldAccess(t *testing.T) { in := `{"foo":"baz","bar":"quux-mooz"}` out := `"quuxMooz"` - res, err := NewJq().WithLibPath("./testdata/jq_lib"). + res, err := newSimpleJq().WithLibPath("./testdata/jq_lib"). Program(prg).Run(in) g.Expect(err).ShouldNot(HaveOccurred()) g.Expect(res).To(Equal(out)) } +func Test_LibPath_Different(t *testing.T) { + g := NewWithT(t) + + invoker := newSimpleJq() + + prg := `include "camel"; .bar | camel` + in := `{"foo":"baz","bar":"quux-mooz"}` + out := `"quuxMooz"` + + res, err := invoker.WithLibPath("./testdata/jq_lib"). + Program(prg).Run(in) + g.Expect(err).ShouldNot(HaveOccurred()) + g.Expect(res).To(Equal(out)) + + prg2 := `include "camel2"; .foobar | camel2` + in2 := `{"baz":"foo","foobar":"qwe-asd-zcx"}` + out2 := `"qweAsdZcx"` + + res, err = invoker.WithLibPath("./testdata/jq_lib_2"). + Program(prg2).Run(in2) + g.Expect(err).ShouldNot(HaveOccurred()) + g.Expect(res).To(Equal(out2)) +} + func Test_CachedProgram_FieldAccess(t *testing.T) { g := NewWithT(t) - p, err := NewJq().WithCache(JqDefaultCache()). + p, err := newSimpleJq(). Program(".foo").Precompile() g.Expect(err).ShouldNot(HaveOccurred()) @@ -59,6 +97,8 @@ func Test_CachedProgram_FieldAccess(t *testing.T) { func Test_Concurrent_FieldAccess(t *testing.T) { g := NewWithT(t) + _, cache, caller := newJq() + job := func() { for i := 0; i < 50; i++ { prg := fmt.Sprintf(`include "camel"; .foo%d | camel`, i) @@ -66,8 +106,7 @@ func Test_Concurrent_FieldAccess(t *testing.T) { out := fmt.Sprintf(`"quuxBaz%d"`, i) in := fmt.Sprintf(`{"foo%d":%s}`, i, val) - res, err := NewJq(). - WithCache(JqDefaultCache()). + res, err := NewJq().WithCache(cache).WithCgoCaller(caller). WithLibPath("./testdata/jq_lib"). Program(prg).Cached().Run(in) g.Expect(err).ShouldNot(HaveOccurred()) @@ -102,11 +141,13 @@ func Test_Concurrent_FieldAccess(t *testing.T) { // Crash is happened when there is only try portion and fromjson is used. // func Test_jq_errors_inside_try_crash_subsequent_runs(t *testing.T) { + caller := NewCgoCaller() + cache := NewJqCache() var r string var err error - r, err = NewJq().WithCache(JqDefaultCache()). + r, err = NewJq().WithCache(cache).WithCgoCaller(caller). Program(`.foo`). Run(`{"foo":"baz"}`) if err != nil { @@ -114,7 +155,7 @@ func Test_jq_errors_inside_try_crash_subsequent_runs(t *testing.T) { } fmt.Println(r) - r, err = NewJq().WithCache(JqDefaultCache()). + r, err = NewJq().WithCache(cache). Program(` try(.data.b64String |= (. | fromjson)) catch . `). @@ -127,7 +168,7 @@ try(.data.b64String |= (. | fromjson)) catch . fmt.Println(r) // This call crashes with trace on jq master - r, err = NewJq().WithCache(JqDefaultCache()). + r, err = NewJq().WithCache(cache).WithCgoCaller(caller). Program(`.foo`). Run(`{"foo":"bar"}`) if err != nil { @@ -136,12 +177,14 @@ try(.data.b64String |= (. | fromjson)) catch . fmt.Println(r) } -func Test_jq_errors_inside_try_crash_subsequent_runs_tonumber(t *testing.T) { +func Test_jq_errors_inside_try_should_not_crash_subsequent_runs_tonumber(t *testing.T) { + caller := NewCgoCaller() + cache := NewJqCache() var r string var err error - r, err = NewJq().WithCache(JqDefaultCache()). + r, err = NewJq().WithCache(cache).WithCgoCaller(caller). Program(`.foo`). Run(`{"foo":"baz"}`) if err != nil { @@ -149,9 +192,11 @@ func Test_jq_errors_inside_try_crash_subsequent_runs_tonumber(t *testing.T) { } fmt.Println(r) - prg, err := NewJq(). //WithCache(JqDefaultCache()). - Program(` -.|tonumber + prg, err := NewJq(). + //WithCache(cache). + WithCgoCaller(caller). + Program(` +try (.|tonumber) `).Precompile() if err != nil { t.Errorf("2: %s", err) @@ -163,7 +208,8 @@ func Test_jq_errors_inside_try_crash_subsequent_runs_tonumber(t *testing.T) { } fmt.Println(r) - prg2, err := NewJq().WithCache(JqDefaultCache()).Program(`.`).Precompile() + prg2, err := NewJq().WithCache(cache).WithCgoCaller(caller). + Program(`.`).Precompile() if err != nil { t.Errorf("3 compile: %s", err) } @@ -185,6 +231,9 @@ func Test_LongRunner_BigData(t *testing.T) { t.SkipNow() g := NewWithT(t) + caller := NewCgoCaller() + cache := NewJqCache() + parallelism := 16 // There are `parallelism` of different programs and fooXXX fields, @@ -197,8 +246,7 @@ func Test_LongRunner_BigData(t *testing.T) { out := fmt.Sprintf(`"quuxBaz%d"`, i%parallelism) in := fmt.Sprintf(`{"foo%d":%s, "extra":%s}`, i%parallelism, val, generateBigJsonObject(1024, i)) - res, err := NewJq(). - WithCache(JqDefaultCache()). + res, err := NewJq().WithCache(cache).WithCgoCaller(caller). WithLibPath("./testdata/jq_lib"). Program(prg).Cached().Run(in) g.Expect(err).ShouldNot(HaveOccurred()) diff --git a/pkg/jq/testdata/jq_lib_2/camel2.jq b/pkg/jq/testdata/jq_lib_2/camel2.jq new file mode 100644 index 0000000..7f15760 --- /dev/null +++ b/pkg/jq/testdata/jq_lib_2/camel2.jq @@ -0,0 +1,2 @@ +def camel2: + gsub("-(?[a-z])"; .a|ascii_upcase); diff --git a/pkg/libjq/jq_state.go b/pkg/libjq/jq_state.go index 89c1373..55c4fae 100644 --- a/pkg/libjq/jq_state.go +++ b/pkg/libjq/jq_state.go @@ -68,6 +68,10 @@ import ( "unsafe" ) +// JqState is a thin wrapper for jq_init, jq_set_attr, jq_compile and jq_start. +// +// It is responsibility of a higher level to call JqState methods in one thread +// as libjq is not compatible with Go's thread migration. type JqState struct { state *C.struct_jq_state } @@ -92,6 +96,7 @@ func (jq *JqState) SetLibraryPath(path string) { C.jq_set_attr(jq.state, JvString("JQ_LIBRARY_PATH"), JvArray(JvString(path))) } +// Compile func (jq *JqState) Compile(program string) error { cProgram := C.CString(program) defer C.free(unsafe.Pointer(cProgram)) diff --git a/pkg/libjq/jv.go b/pkg/libjq/jv.go index 4a293ce..4cab946 100644 --- a/pkg/libjq/jv.go +++ b/pkg/libjq/jv.go @@ -23,9 +23,8 @@ func JvString(str string) C.jv { } // JvArray returns a jv array value. jq sources has JV_ARRAY macros for this. -func JvArray(first C.jv, items ...C.jv) C.jv { +func JvArray(items ...C.jv) C.jv { arr := C.jv_array() - arr = C.jv_array_append(arr, first) for _, item := range items { arr = C.jv_array_append(arr, item) }