Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Avoid using cfunction runtime closures in iteration callback #812

Merged
merged 7 commits into from
Feb 10, 2021
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
4 changes: 2 additions & 2 deletions src/HDF5.jl
Original file line number Diff line number Diff line change
Expand Up @@ -942,7 +942,7 @@ name(attr::Attribute) = h5a_get_name(attr)
function Base.keys(x::Union{Group,File})
checkvalid(x)
children = sizehint!(String[], length(x))
h5l_iterate(x, H5_INDEX_NAME, H5_ITER_INC) do group, name, info, data
musm marked this conversation as resolved.
Show resolved Hide resolved
h5l_iterate(x, H5_INDEX_NAME, H5_ITER_INC) do _, name, _
push!(children, unsafe_string(name))
return herr_t(0)
end
Expand All @@ -952,7 +952,7 @@ end
function Base.keys(x::Attributes)
checkvalid(x.parent)
children = sizehint!(String[], length(x))
h5a_iterate(x.parent, H5_INDEX_NAME, H5_ITER_INC) do loc_id, attr_name, ainfo, data
h5a_iterate(x.parent, H5_INDEX_NAME, H5_ITER_INC) do _, attr_name, _
push!(children, unsafe_string(attr_name))
return herr_t(0)
end
Expand Down
81 changes: 75 additions & 6 deletions src/api_helpers.jl
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,46 @@ function h5a_get_name_by_idx(loc_id, obj_name, idx_type, order, idx, lapl_id)
return String(buf)
end

function h5a_iterate(f, obj_id, idx_type, order, idx = 0)
# libhdf5 supports proper closure environments, so we use that support rather than
# emulating it with the less desirable form of creating closure handles directly in
# `@cfunction` with `$f`.
# This helper translates between the two preferred forms for each respective language.
function h5a_iterate_helper(loc_id::hid_t, attr_name::Ptr{Cchar}, ainfo::Ptr{H5A_info_t}, @nospecialize(f::Any))::herr_t
return f(loc_id, attr_name, ainfo)
end
"""
h5a_iterate(f, loc_id, idx_type, order, idx = 0) -> HDF5.hsize_t

Executes [`h5a_iterate`](@ref h5a_iterate(::hid_t, ::Cint, ::Cint, ::Ptr{hsize_t}, ::Ptr{Cvoid}, ::Ptr{Cvoid}))
with the user-provided callback function `f`, returning the index where iteration ends.

The callback function must correspond to the signature
```
f(loc::HDF5.hid_t, name::Ptr{Cchar}, info::Ptr{HDF5.H5A_info_t}) -> HDF5.herr_t
```
where a negative return value halts iteration abnormally, a positive value halts iteration
successfully, and zero continues iteration.

# Examples
```julia-repl
julia> HDF5.h5a_iterate(obj, HDF5.H5_INDEX_NAME, HDF5.H5_ITER_INC) do loc, name, info
println(unsafe_string(name))
return HDF5.herr_t(0)
end
```
"""
function h5a_iterate(@nospecialize(f), obj_id, idx_type, order, idx = 0)
idxref = Ref{hsize_t}(idx)
fptr = @cfunction($f, herr_t, (hid_t, Ptr{Cchar}, Ptr{H5A_info_t}, Ptr{Cvoid}))
h5a_iterate(obj_id, idx_type, order, idxref, fptr, C_NULL)
fptr = @cfunction(h5a_iterate_helper, herr_t, (hid_t, Ptr{Cchar}, Ptr{H5A_info_t}, Any))
userf = Ref{Any}(f)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it matter that this is a Ref instead of a Ptr{Any} thereby eliminating the unsafe_convert below? Presumably Ref is the better option here since f is a method defined on the Julia end, which more closely aligns with the general usage of Ref.

Copy link

@vchuravy vchuravy Feb 8, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there are two issues here:

  1. Functions are immutable and thus you can't use Base.pointer_from_objref.
  2. Ideally you would want the signature of the ccall to be ::Any and thus have Julia take care of the implicit conversion

I am a bit sketched out by the Ptr{Any} which then is converted to Any on the callee side, but I think Ref{Any} in ccall/ cfunction has special handling.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I'm a bit unsure of the dance of Ptr{Any}, Ref{Any}, Any (and having put this up over two weeks ago, I forget what some of my problems/conclusions were). I think it's the special-cased handling mentioned in the Ref docstring that was throwing me off for a while, and this is what I got to work:

As a special case, setting T = Any will instead cause the creation of a pointer to the reference itself when converted to a Ptr{Any} (a jl_value_t const* const* if T is immutable, else a jl_value_t *const *). When converted to a Ptr{Cvoid}, it will still return a pointer to the data region as for any other T.

Since @vtjnash just commented here, though, would you mind giving an expert opinion on what the cleanest way to do this would be?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Simply put, you need to use the same expression in both places. That can be Ref{T} or Any (or perhaps even Ref{Any}, but that seems like usually an odd choice)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I simplified the signature in the @cfunction call and then the argument being passed through to the low-level h5(a|l)_iterate. (Not sure how I skipped over that particular variation a couple of weeks ago considering it is the simple option...)

If I'm understanding correctly, though, I do still need the userf = Ref{Any}(f) line so that the closure has a pointer address for the ccall to make use of.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you need that? You've used cfunction with Any, so you must use ccall with Any also

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without the Ref container, there are MethodErrors due to no unsafe_convert{::Type{Ptr{Nothing}}, ::<closure type>).

diff --git a/src/api_helpers.jl b/src/api_helpers.jl
index f5ba65e..864230b 100644
--- a/src/api_helpers.jl
+++ b/src/api_helpers.jl
@@ -68,15 +68,7 @@ julia> HDF5.h5a_iterate(obj, HDF5.H5_INDEX_NAME, HDF5.H5_ITER_INC) do loc, name,
 function h5a_iterate(@nospecialize(f), obj_id, idx_type, order, idx = 0)
     idxref = Ref{hsize_t}(idx)
     fptr = @cfunction(h5a_iterate_helper, herr_t, (hid_t, Ptr{Cchar}, Ptr{H5A_info_t}, Any))
-    userf = Ref{Any}(f)
-    if VERSION < v"1.6.0-DEV.1038"
-        # unsafe_convert(Ptr{Cvoid}, ::RefValue{Any}) returns pointer to RefValue instead
-        # of data --- see JuliaLang/julia#37591
-        userfptr = unsafe_load(Ptr{Ptr{Cvoid}}(unsafe_convert(Ptr{Any}, userf)))
-        GC.@preserve userf h5a_iterate(obj_id, idx_type, order, idxref, fptr, userfptr)
-    else
-        GC.@preserve userf h5a_iterate(obj_id, idx_type, order, idxref, fptr, userf)
-    end
+    h5a_iterate(obj_id, idx_type, order, idxref, fptr, f)
     return idxref[]
 end
 
@@ -204,15 +196,7 @@ julia> HDF5.h5l_iterate(hfile, HDF5.H5_INDEX_NAME, HDF5.H5_ITER_INC) do group, n
 function h5l_iterate(@nospecialize(f), group_id, idx_type, order, idx = 0)
     idxref = Ref{hsize_t}(idx)
     fptr = @cfunction(h5l_iterate_helper, herr_t, (hid_t, Ptr{Cchar}, Ptr{H5L_info_t}, Any))
-    userf = Ref{Any}(f)
-    if VERSION < v"1.6.0-DEV.1038"
-        # unsafe_convert(Ptr{Cvoid}, ::RefValue{Any}) returns pointer to RefValue instead
-        # of data --- see JuliaLang/julia#37591
-        userfptr = unsafe_load(Ptr{Ptr{Cvoid}}(unsafe_convert(Ptr{Any}, userf)))
-        GC.@preserve userf h5l_iterate(group_id, idx_type, order, idxref, fptr, userfptr)
-    else
-        GC.@preserve userf h5l_iterate(group_id, idx_type, order, idxref, fptr, userf)
-    end
+    h5l_iterate(group_id, idx_type, order, idxref, fptr, f)
     return idxref[]
 end
julia> using HDF5
[ Info: Precompiling HDF5 [f67ccb44-e63f-5c2f-98bd-6dc0ccc4ba2f]

julia> hid = h5open("/tmp/test.h5", "w");

julia> hid["test"] = 1;

julia> hid
Error showing value of type HDF5.File:
ERROR: MethodError: no method matching unsafe_convert(::Type{Ptr{Nothing}}, ::HDF5.var"#10#14"{IOContext{IOBuffer}, String, HDF5.var"#depth_check#13"{Int64, IOContext{IOBuffer}, Int64, Bool}, Int64, String, String})
Closest candidates are:
  unsafe_convert(::Union{Type{Ptr{Nothing}}, Type{Ptr{Base.Libc.FILE}}}, ::Base.Libc.FILE) at libc.jl:94
  unsafe_convert(::Type{Ptr{T}}, ::SharedArrays.SharedArray{T, N} where N) where T at /buildworker/worker/package_linux64/build/usr/share/julia/stdlib/v1.6/SharedArrays/src/SharedArrays.jl:354
  unsafe_convert(::Type{Ptr{T}}, ::SharedArrays.SharedArray) where T at /buildworker/worker/package_linux64/build/usr/share/julia/stdlib/v1.6/SharedArrays/src/SharedArrays.jl:355
  ...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh sorry, I misunderstood and answered the wrong question.

We've been following the C api, so the ccall wrapper function's are declared with void* / Ptr{Cvoid} arguments. Are you saying we should change that to Any instead?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that is the wrong decl.

there are MethodErrors

This is intentional, as otherwise you're missing the GC root (as detected by the miscompilation assert)

if VERSION < v"1.6.0-DEV.1038"
# unsafe_convert(Ptr{Cvoid}, ::RefValue{Any}) returns pointer to RefValue instead
# of data --- see JuliaLang/julia#37591
userfptr = unsafe_load(Ptr{Ptr{Cvoid}}(unsafe_convert(Ptr{Any}, userf)))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sadly this is causing issues on 1.4

Assertion failed: isSpecialPtr(V->getType()), file /cygdrive/d/buildbot/worker/package_win64/build/src/llvm-late-gc-lowering.cpp, line 773

signal (22): SIGABRT
in expression starting at C:\Users\Mus\.julia\dev\HDF5\test\plain.jl:5
crt_sig_handler at /cygdrive/d/buildbot/worker/package_win64/build/src\signals-win.c:92

exec_options at .\client.jl:264
_start at .\client.jl:484
jfptr__start_2087.clone_1 at C:\Users\Mus\AppData\Local\Programs\Julia\Julia-1.4.2\lib\julia\sys.dll (unknown line)
unknown function (ip: 00000000004017E1)
unknown function (ip: 0000000000401BD6)
unknown function (ip: 00000000004013DE)
unknown function (ip: 000000000040151A)
BaseThreadInitThunk at C:\WINDOWS\System32\KERNEL32.DLL (unknown line)
RtlUserThreadStart at C:\WINDOWS\SYSTEM32\ntdll.dll (unknown line)
Allocations: 19101740 (Pool: 19098255; Big: 3485); GC: 20
...

GC.@preserve userf h5a_iterate(obj_id, idx_type, order, idxref, fptr, userfptr)
else
GC.@preserve userf h5a_iterate(obj_id, idx_type, order, idxref, fptr, userf)
end
return idxref[]
end

Expand Down Expand Up @@ -140,10 +176,43 @@ function h5l_get_name_by_idx(loc_id, group_name, idx_type, order, idx, lapl_id)
return String(buf)
end

function h5l_iterate(f, group_id, idx_type, order, idx = 0)
# See explanation for h5a_iterate above.
function h5l_iterate_helper(group::hid_t, name::Ptr{Cchar}, info::Ptr{H5L_info_t}, @nospecialize(f::Any))::herr_t
return f(group, name, info)
end
"""
h5l_iterate(f, group_id, idx_type, order, idx = 0) -> HDF5.hsize_t

Executes [`h5l_iterate`](@ref h5l_iterate(::hid_t, ::Cint, ::Cint, ::Ptr{hsize_t}, ::Ptr{Cvoid}, ::Ptr{Cvoid}))
with the user-provided callback function `f`, returning the index where iteration ends.

The callback function must correspond to the signature
```
f(group::HDF5.hid_t, name::Ptr{Cchar}, info::Ptr{HDF5.H5L_info_t}) -> HDF5.herr_t
```
where a negative return value halts iteration abnormally, a positive value halts iteration
successfully, and zero continues iteration.

# Examples
```julia-repl
julia> HDF5.h5l_iterate(hfile, HDF5.H5_INDEX_NAME, HDF5.H5_ITER_INC) do group, name, info
println(unsafe_string(name))
return HDF5.herr_t(0)
end
```
"""
function h5l_iterate(@nospecialize(f), group_id, idx_type, order, idx = 0)
idxref = Ref{hsize_t}(idx)
fptr = @cfunction($f, herr_t, (hid_t, Ptr{Cchar}, Ptr{H5L_info_t}, Ptr{Cvoid}))
h5l_iterate(group_id, idx_type, order, idxref, fptr, C_NULL)
fptr = @cfunction(h5l_iterate_helper, herr_t, (hid_t, Ptr{Cchar}, Ptr{H5L_info_t}, Any))
userf = Ref{Any}(f)
if VERSION < v"1.6.0-DEV.1038"
# unsafe_convert(Ptr{Cvoid}, ::RefValue{Any}) returns pointer to RefValue instead
# of data --- see JuliaLang/julia#37591
userfptr = unsafe_load(Ptr{Ptr{Cvoid}}(unsafe_convert(Ptr{Any}, userf)))
GC.@preserve userf h5l_iterate(group_id, idx_type, order, idxref, fptr, userfptr)
else
GC.@preserve userf h5l_iterate(group_id, idx_type, order, idxref, fptr, userf)
end
return idxref[]
end

Expand Down
4 changes: 2 additions & 2 deletions src/show.jl
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ function _show_tree(io::IO, obj::Union{File,Group,Dataset,Datatype,Attributes,At

if attributes && !isa(obj, Attribute)
obj′ = obj isa Attributes ? obj.parent : obj
h5a_iterate(obj′, H5_INDEX_NAME, H5_ITER_INC) do _, cname, _, _
h5a_iterate(obj′, H5_INDEX_NAME, H5_ITER_INC) do _, cname, _
depth_check() && return herr_t(1)

name = unsafe_string(cname)
Expand All @@ -206,7 +206,7 @@ function _show_tree(io::IO, obj::Union{File,Group,Dataset,Datatype,Attributes,At

typeof(obj) <: Union{File, Group} || return nothing

h5l_iterate(obj, H5_INDEX_NAME, H5_ITER_INC) do loc_id, cname, _, _
h5l_iterate(obj, H5_INDEX_NAME, H5_ITER_INC) do loc_id, cname, _
depth_check() && return herr_t(1)

name = unsafe_string(cname)
Expand Down