Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
go: ['1.22.x', '1.23.x', '1.24.x']
go: ['1.22.x', '1.23.x', '1.24.x', '1.25.x']
platform: [ubuntu-latest, windows-latest, macos-latest, macos-14]
steps:
- uses: actions/checkout@v5
Expand Down
25 changes: 25 additions & 0 deletions .goreleaser.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
project_name: qjs
dist: releases

version: 1

before:
hooks:
- go mod tidy

builds:
- skip: true

archives:
- id: build_main
format: zip

snapshot:
name_template: "{{ incpatch .Version }}-next"

changelog:
sort: asc
filters:
exclude:
- "^docs:"
- "^test:"
14 changes: 7 additions & 7 deletions common.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,19 @@ import (
)

const (
// The minimum number of arguments for Map forEach callback (value, key).
// MinMapForeachArgs is the minimum number of arguments for Map forEach callback (value, key).
MinMapForeachArgs = 2
// The conversion factor from nanoseconds to milliseconds.
// NanosToMillis is the conversion factor from nanoseconds to milliseconds.
NanosToMillis = 1e6
// The size of a uint64 value in bytes.
// Uint64ByteSize is the size of a uint64 value in bytes.
Uint64ByteSize = 8
// The bit position of the sign bit in a 64-bit unsigned integer.
// Uint64SignBitPosition is the bit position of the sign bit in a 64-bit unsigned integer.
Uint64SignBitPosition = 63
// The size in bytes of a packed pointer structure.
// PackedPtrSize is the size in bytes of a packed pointer structure.
PackedPtrSize = 8
// NullPtr represents a null pointer value.
NullPtr = uint32(0)
// The null terminator byte for C-style strings.
// StringTerminator is the null terminator byte for C-style strings.
StringTerminator = byte(0)
)

Expand Down Expand Up @@ -870,7 +870,7 @@ func ParseTimezone(tz string) *time.Location {
return time.UTC
}

// Check if the platform is 32-bit by comparing the size of uintptr.
// Is32BitPlatform check if the platform is 32-bit by comparing the size of uintptr.
func Is32BitPlatform() bool {
return strconv.IntSize == 32
}
Expand Down
2 changes: 1 addition & 1 deletion context.go
Original file line number Diff line number Diff line change
Expand Up @@ -356,7 +356,7 @@ func (c *Context) Function(fn Function, isAsyncs ...bool) *Value {

fnID := c.runtime.registry.Register(fn)
ctxID := c.runtime.registry.Register(c)
proxyFuncVal := c.Call("QJS_CreateProxyFunction", c.Raw(), fnID, ctxID, isAsync)
proxyFuncVal := c.Call("QJS_CreateFunctionProxy", c.Raw(), fnID, ctxID, isAsync)

// Registry: Store IDs for cleanup and callback identification
proxyFuncVal.SetPropertyStr("__fnID", c.NewInt64(int64(fnID)))
Expand Down
2 changes: 1 addition & 1 deletion eval_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,7 @@ func TestModuleLoading(t *testing.T) {
options: []qjs.EvalOptionFunc{qjs.TypeModule()},
expectErr: func(t *testing.T, err error) {
assert.Error(t, err)
assert.Contains(t, err.Error(), "could not load file")
assert.Contains(t, err.Error(), "No such file or directory: testdata/00_loader/not_found.js")
},
},
{
Expand Down
59 changes: 58 additions & 1 deletion functojs.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package qjs

import (
"context"
"fmt"
"reflect"
)
Expand Down Expand Up @@ -125,13 +126,69 @@ func handlePointerArgument(jsArg *Value, argType reflect.Type) (reflect.Value, e
return ptrVal, nil
}

// CreateNonNilSample creates appropriate non-nil samples for types that have nil zero values.
func CreateNonNilSample(argType reflect.Type) any {
switch argType.Kind() {
case reflect.Interface:
// Special handling for context.Context
if argType.Implements(reflect.TypeOf((*context.Context)(nil)).Elem()) {
return context.Background()
}

// For other interfaces, return nil to let the conversion
// use the default logic which can handle dynamic type inference
return nil

case reflect.Ptr:
elemType := argType.Elem()
elemZero := reflect.Zero(elemType)
ptr := reflect.New(elemType)
ptr.Elem().Set(elemZero)

return ptr.Interface()

case reflect.Array:
return reflect.New(argType).Elem().Interface()

case reflect.Slice:
return reflect.MakeSlice(argType, 0, 0).Interface()

case reflect.Map:
return reflect.MakeMap(argType).Interface()

case reflect.Chan:
return reflect.MakeChan(argType, 1).Interface() // size 1 buffer to avoid blocking

case reflect.Func:
return createDummyFunction(argType)

default:
// For other types (shouldn't happen), return zero value
return reflect.Zero(argType).Interface()
}
}

// createDummyFunction creates a dummy function with the specified signature for type inference.
func createDummyFunction(funcType reflect.Type) any {
fn := reflect.MakeFunc(funcType, func(_ []reflect.Value) []reflect.Value {
results := make([]reflect.Value, funcType.NumOut())
for i := range funcType.NumOut() {
results[i] = reflect.Zero(funcType.Out(i))
}

return results
})

return fn.Interface()
}

// JsArgToGo converts a single JS argument to a Go value with enhanced type handling.
func JsArgToGo(jsArg *Value, argType reflect.Type) (reflect.Value, error) {
if argType.Kind() == reflect.Ptr {
return handlePointerArgument(jsArg, argType)
}

goZeroVal := reflect.Zero(argType).Interface()
goZeroVal := CreateNonNilSample(argType)

goVal, err := JsValueToGo(jsArg, goZeroVal)
if err != nil {
Expand Down
75 changes: 75 additions & 0 deletions functojs_test.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package qjs_test

import (
"context"
"errors"
"fmt"
"reflect"
"testing"
"unsafe"

Expand Down Expand Up @@ -641,3 +643,76 @@ func TestMethodBinding(t *testing.T) {
})
})
}

func TestCreateNonNilSample(t *testing.T) {
tests := []struct {
name string
input any
expectedType any
shouldBeNotNil bool
testFuncExecution bool
}{
{
name: "Context type",
input: context.Background(),
expectedType: context.Background(),
shouldBeNotNil: true,
},
{
name: "Pointer type",
input: (*int)(nil),
expectedType: (*int)(nil),
shouldBeNotNil: false, // Pointer itself can be nil but sample won't be
},
{
name: "Array type",
input: [3]string{},
expectedType: [3]string{},
shouldBeNotNil: true,
},
{
name: "Slice type",
input: []string{},
expectedType: []string{},
shouldBeNotNil: true,
},
{
name: "Map type",
input: map[string]int{},
expectedType: map[string]int{},
shouldBeNotNil: true,
},
{
name: "Chan type",
input: (chan int)(nil),
expectedType: (chan int)(nil),
shouldBeNotNil: true,
},
{
name: "Func type",
input: (func(int) int)(nil),
expectedType: (func(int) int)(nil),
shouldBeNotNil: true,
testFuncExecution: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
sample := qjs.CreateNonNilSample(reflect.TypeOf(tt.input))

assert.IsType(t, tt.expectedType, sample)

if tt.shouldBeNotNil {
assert.NotNil(t, sample)
}

// Special test for function execution
if tt.testFuncExecution {
dummyFunc := sample.(func(int) int)
result := dummyFunc(42)
assert.Equal(t, 0, result) // Should return zero value
}
})
}
}
8 changes: 6 additions & 2 deletions gotojs.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ func (tracker *Tracker[T]) StructToJSObjectValue(
rval reflect.Value,
) (*Value, error) {
return withJSObject(c, func(obj *Value) error {
// obj.SetPropertyStr("__go_type", c.NewString(GetGoTypeName(rtype)))

// Determine the struct type and value for field processing:
// - pointer to struct: dereference for field processing.
// - direct struct: use as-is.
Expand All @@ -50,7 +52,7 @@ func (tracker *Tracker[T]) StructToJSObjectValue(
structVal reflect.Value
)

if rtype.Kind() == reflect.Ptr {
if rtype.Kind() == reflect.Pointer {
structType = rtype.Elem()
structVal = rval.Elem()
} else {
Expand Down Expand Up @@ -360,8 +362,9 @@ func (tracker *Tracker[T]) processEmbeddedFields(
) error {
for i := range rtype.NumField() {
field := rtype.Field(i)
jsonIgnore := field.Tag.Get("json") == "-"

if !field.IsExported() || !field.Anonymous {
if !field.IsExported() || !field.Anonymous || jsonIgnore {
continue
}

Expand Down Expand Up @@ -434,6 +437,7 @@ func (tracker *Tracker[T]) processRegularFields(
) error {
for i := range rtype.NumField() {
field := rtype.Field(i)

if !field.IsExported() || field.Anonymous {
continue
}
Expand Down
1 change: 0 additions & 1 deletion handle.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,6 @@ func ConvertToUnsigned[T Unsigned](h *Handle) T {
return result
}

// Integer conversion methods using the generic functions.
func (h *Handle) Int() int { return ConvertToSigned[int](h) }
func (h *Handle) Int8() int8 { return ConvertToSigned[int8](h) }
func (h *Handle) Int16() int16 { return ConvertToSigned[int16](h) }
Expand Down
4 changes: 2 additions & 2 deletions jstogo.go
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ func jsObjectOrMapToGoMap[T any](
temp, obj, sample, targetType := createGoObjectTarget(input, samples...)

defer func() {
v, err = processTempValue("JsObjectOrMapToGo", temp, err, sample)
v, err = processTempValue("JsObjectOrMapToGoMap", temp, err, sample)
}()

var ct CircularTracker[uint64]
Expand Down Expand Up @@ -270,7 +270,7 @@ func jsObjectOrMapToGoStruct[T any](
temp, obj, sample, targetType := createGoObjectTarget(input, samples...)

defer func() {
v, err = processTempValue("JsObjectOrMapToGo", temp, err, sample)
v, err = processTempValue("JsObjectOrMapToGoStruct", temp, err, sample)
}()

var ct CircularTracker[uint64]
Expand Down
4 changes: 2 additions & 2 deletions options.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ const (
JsEvalFlagStrict = (1 << 3)
// JsEvalFlagUnUsed is reserved for future use.
JsEvalFlagUnUsed = (1 << 4)
// Compile-only; returns a JS bytecode/module for JS_EvalFunction()..
// JsEvalFlagCompileOnly returns a JS bytecode/module for JS_EvalFunction().
JsEvalFlagCompileOnly = (1 << 5)
// Don't include the stack frames before this eval in the Error() backtraces.
// JsEvalFlagBackTraceBarrier prevents the stack frames before this eval in the Error() backtraces.
JsEvalFlagBackTraceBarrier = (1 << 6)
// JsEvalFlagAsync enables top-level await (global scope only).
JsEvalFlagAsync = (1 << 7)
Expand Down
Binary file modified qjsextra.wasm
Binary file not shown.
Loading
Loading