jacobwilliams / json-fortran

A Modern Fortran JSON API
https://jacobwilliams.github.io/json-fortran/
Other
333 stars 82 forks source link

After creating an new object, any remaining key-value pairs in origin json_file will be erased. #564

Closed LaplaceSoda closed 2 months ago

LaplaceSoda commented 2 months ago

After creating an new object of json_file type from one json_file object, any remaining key-value pairs in origin json will be erased.

Environment

gfortran 13.2.0 (Built by MSYS2 project) fpm 0.10.0, alpha

Reproduction

Here is the test.json

{
    "aaa": {
        "bbb": 1,
        "ccc": {
            "ddd": 2
        },
        "eee": 3,
        "fff": 4.5
    }
}

We try to get something from it in this main.f90 and separate a portion (which could be used as an input for another function)

program main
    use json_module
    implicit none

    type(json_file) :: temp_json
    type(json_value), pointer :: value
    type(json_file) :: new_json
    logical :: found

    call temp_json%initialize(path_mode=2)
    call temp_json%load('test.json')

    call temp_json%print()

    call temp_json%get("/aaa/ccc", value, found)
    new_json = json_file(value, path_mode=2)
    call new_json%print()
    ! call other_sub(new_json)

    call temp_json%print()
end program main

The result shows that something in origin json_file object has been removed after a new object is created. And we can no longer get any things from it. image

jacobwilliams commented 2 months ago

I'm not seeing with with gfortran 12.3.0. I get:

{
  "aaa": {
    "bbb": 1,
    "ccc": {
      "ddd": 2
    },
    "eee": 3,
    "fff": 0.45E+1
  }
}
{
  "ddd": 2
}
{
  "aaa": {
    "bbb": 1,
    "ccc": {
      "ddd": 2
    },
    "eee": 3,
    "fff": 0.45E+1
  }
}

I can try to test with gfortran 13 when I get a chance...

LaplaceSoda commented 2 months ago

At first I thought using

call new_json%initialize(path_mode=2)
call new_json%add(value)

can solve the problem. But apparently I am wrong, in this more complex main.f90, the above problem arises again.

program main
    use json_module
    implicit none

    type(json_file) :: json
    logical :: found
    integer :: i

    call json%initialize(path_mode=2)
    call json%load('test.json')

    call test(json)

    print *, 'json after test():'
    call json%print()
    call json%get('/aaa/eee', i, found=found)
    print *, 'e=', i
    call json%get('/aaa/ccc/ddd', i, found=found)
    print *, 'd=', i
contains
    subroutine test(json)
        type(json_file) :: json
        type(json_file) :: new_json
        type(json_value), pointer :: value
        logical :: found

        print *, "test's json get from main:"
        call json%print()

        call json%get("/aaa/ccc", value, found)
        call new_json%initialize(path_mode=2)
        call new_json%add(value)

        print *, "new_json before test01():"
        call new_json%print()
        call test01(new_json)
        print *, "new_json after test01():"
        call new_json%print()
        print *, "json after test01():"
        call json%print()
    end subroutine test

    subroutine test01(json)
        type(json_file) :: json
        integer :: d
        print *, "test01's json get from test:"
        call json%print()
        call json%get('/ddd', d, found=found)
        print *, 'd=', d
    end subroutine test01
end program main

And here are the results:

 test's json get from main:
{
  "aaa": {
    "bbb": 1,
    "ccc": {
      "ddd": 2
    },
    "eee": 3,
    "fff": 0.45E+1
  }
}
 new_json before test01():
{
  "ddd": 2
}
 test01's json get from test:
{
  "ddd": 2
}
 d=           2
 new_json after test01():
{
  "ddd": 2
}
 json after test01():
{
  "aaa": {
    "bbb": 1,
    "ccc": {
      "ddd": 2
    },
    "eee": 3,
    "fff": 0.45E+1
  }
}
 json after test():
{
  "aaa": {
    "bbb": 1,
 e=           0
 d=           0

So what should I do to pass parts of the json to the subroutine safely without affecting the original json_file object?

LaplaceSoda commented 2 months ago

My guess is that this issue is related to the destructor, but I don't know how to avoid it.

LaplaceSoda commented 2 months ago

Tests found that after passing the json_file object (or a certain part) to the subfunction for processing,destroying the original object (automatically by finalizer) would also raise an exception, causing the program to exit.

jacobwilliams commented 2 months ago

The issue here is that you are extracting a pointer from one json_file, putting it into another json_file, which then gets destroyed when that variable goes out of scope. the two options you have when you do this are to make a deep copy of the pointer before putting it in the 2nd structure, or nullifying the pointer in the 2nd structure before it goes out of scope. I'll try to make an example...

Option 1:

    subroutine test(json)
        type(json_file) :: json
        type(json_file) :: new_json
        type(json_value), pointer :: value, value_clone
        type(json_core) :: j
        logical :: found

        print *, "test's json get from main:"
        call json%print()
        call json%get("/aaa/ccc", value, found)
        call j%clone(value, value_clone)  ! make a deep copy of this, so we dont destroy part of the original structure when new_json goes out of scope
        call new_json%initialize(path_mode=2)
        call new_json%add(value_clone)
        print *, "new_json before test01():"
        call new_json%print()
        call test01(new_json)
        print *, "new_json after test01():"
        call new_json%print()
        print *, "json after test01():"
        call json%print()
    end subroutine test

Option 2:

    subroutine test(json)
        type(json_file) :: json
        type(json_file) :: new_json
        type(json_value), pointer :: value
        logical :: found

        print *, "test's json get from main:"
        call json%print()
        call json%get("/aaa/ccc", value, found)
        call new_json%initialize(path_mode=2)
        call new_json%add(value)
        print *, "new_json before test01():"
        call new_json%print()
        call test01(new_json)
        print *, "new_json after test01():"
        call new_json%print()
        call new_json%nullify()  ! Nullify the pointer because its also in the original structure, we dont want this routine to destroy it.
        print *, "json after test01():"
        call json%print()
    end subroutine test
LaplaceSoda commented 2 months ago

The issue here is that you are extracting a pointer from one json_file, putting it into another json_file, which then gets destroyed when that variable goes out of scope.

Thanks a lot. This is the main reason! It is because of part of the pointer being destroyed in subroutine automatically that the original object can no longer be a normal object and then can no longer use get function or be destroyed normally.

nullifying the pointer in the 2nd structure before it goes out of scope

This way seems more momery-less,so I tryed this way first. And the result shows that it works well! Thank you very much again!

But eventually I found that it seems that we still can't use json_file constructor to new a new_object if we want keep the original json_file object normal (under gfortran 14.1.0-3). My guess is because the constructor will nullify the passed pointer, this part in original object is removed(?). I don't know if it truly affects

jacobwilliams commented 2 months ago

Yes, if you use the json_file(p) constructor, it nullifies the input. if you don't want that, you can use call json_file%add(p) which will leave it alone, but you have to careful since you now have an object that will destroy p when it goes out of scope.

LaplaceSoda commented 2 months ago

you have to careful since you now have an object that will destroy p when it goes out of scope

Yes, now I have learned always using nullify the new object before return to protect the original pointer from being destroyed. Thank you very much.

By the way,it seems that nullify_pointer=.false. don't improve the status while using add(value,destroy_original=.false.) is easier and safer.