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

Add support for automatically calling unsafe_load() in getproperty() #502

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
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: 2 additions & 0 deletions docs/src/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ Changelog](https://keepachangelog.com).
([5a1cc29](https://github.com/JuliaInterop/Clang.jl/commit/5a1cc29c154ed925f01e59dfd705cbf8042158e4)).
- Added bindings for Clang 17, which should allow compatibility with Julia 1.12
([#494]).
- Experimental support for automatically dereferencing struct fields in
`Base.getproperty()` with the `auto_field_dereference` option ([#502]).

### Fixed

Expand Down
13 changes: 13 additions & 0 deletions gen/generator.toml
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,19 @@ wrap_variadic_function = false
# generate getproperty/setproperty! methods for the types in the following list
field_access_method_list = []

# EXPERIMENTAL:
# By default the getproperty!(x::Ptr, ::Symbol) methods created for wrapped
# types will return pointers (Ptr{T}) to the struct fields. That behaviour is
# useful for accessing nested struct fields but it does require explicitly
# calling unsafe_load() every time. When enabled this option will automatically
# call unsafe_load() for you *except on nested struct fields and arrays*, which
# should make explicitly calling unsafe_load() unnecessary in most cases. A @ptr
# macro will be defined for cases where you really do want a pointer to a field
# (e.g. for writing), which supports syntax like `@ptr(foo.bar)`.
#
# This should be used with `field_access_method_list`.
auto_field_dereference = false

# the generator will prefix the function argument names in the following list with a "_" to
# prevent the generated symbols from conflicting with the symbols defined and exported in Base.
function_argument_conflict_symbols = []
Expand Down
142 changes: 88 additions & 54 deletions src/generator/codegen.jl
Original file line number Diff line number Diff line change
Expand Up @@ -296,19 +296,20 @@ end

############################### Struct ###############################

function _emit_getproperty_ptr!(body, root_cursor, cursor, options)
function _emit_pointer_access!(body, root_cursor, cursor, options)
field_cursors = fields(getCursorType(cursor))
field_cursors = isempty(field_cursors) ? children(cursor) : field_cursors
for field_cursor in field_cursors
n = name(field_cursor)
if isempty(n)
_emit_getproperty_ptr!(body, root_cursor, field_cursor, options)
_emit_pointer_access!(body, root_cursor, field_cursor, options)
continue
end
fsym = make_symbol_safe(n)
fty = getCursorType(field_cursor)
ty = translate(tojulia(fty), options)
offset = getOffsetOf(getCursorType(root_cursor), n)

if isBitField(field_cursor)
w = getFieldDeclBitWidth(field_cursor)
@assert w <= 32 # Bit fields should not be larger than int(32 bits)
Expand All @@ -322,12 +323,74 @@ function _emit_getproperty_ptr!(body, root_cursor, cursor, options)
end
end

# Base.getproperty(x::Ptr, f::Symbol) -> Ptr
# getptr(x::Ptr, f::Symbol) -> Ptr
function emit_getptr!(dag, node, options)
sym = make_symbol_safe(node.id)
signature = Expr(:call, :getptr, :(x::Ptr{$sym}), :(f::Symbol))
body = Expr(:block)
_emit_pointer_access!(body, node.cursor, node.cursor, options)

# The default field access exception changed to FieldError in 1.12
throw_expr = :(
@static if VERSION >= v"1.12.0-DEV"
throw(FieldError($sym, f))
else
error($("Unrecognized field of type `$sym`") * ": $f")
end
)
Base.remove_linenums!(throw_expr)
throw_expr.args[2] = nothing # Remove the sticky LineNumberNode from the macro

push!(body.args, throw_expr)
push!(node.exprs, Expr(:function, signature, body))
return dag
end

function emit_deref_getproperty!(body, root_cursor, cursor, options)
field_cursors = fields(getCursorType(cursor))
field_cursors = isempty(field_cursors) ? children(cursor) : field_cursors
for field_cursor in field_cursors
n = name(field_cursor)
if isempty(n)
emit_deref_getproperty!(body, root_cursor, field_cursor, options)
continue
end
fsym = make_symbol_safe(n)
fty = getCursorType(field_cursor)
canonical_type = getCanonicalType(fty)

return_expr = :(getptr(x, f))

# Automatically dereference all field types except for nested structs
# and arrays.
if !(canonical_type isa Union{CLRecord, CLConstantArray}) && !isBitField(field_cursor)
return_expr = :(unsafe_load($return_expr))
elseif isBitField(field_cursor)
return_expr = :(getbitfieldproperty(x, $return_expr))
end

ex = :(f === $(QuoteNode(fsym)) && return $return_expr)
push!(body.args, ex)
end
end

# Base.getproperty(x::Ptr, f::Symbol)
function emit_getproperty_ptr!(dag, node, options)
auto_deref = get(options, "auto_field_dereference", false)
sym = make_symbol_safe(node.id)

# If automatically dereferencing, we first need to emit getptr!()
if auto_deref
emit_getptr!(dag, node, options)
end

signature = Expr(:call, :(Base.getproperty), :(x::Ptr{$sym}), :(f::Symbol))
body = Expr(:block)
_emit_getproperty_ptr!(body, node.cursor, node.cursor, options)
if auto_deref
emit_deref_getproperty!(body, node.cursor, node.cursor, options)
else
_emit_pointer_access!(body, node.cursor, node.cursor, options)
end
push!(body.args, :(return getfield(x, f)))
getproperty_expr = Expr(:function, signature, body)
push!(node.exprs, getproperty_expr)
Expand All @@ -345,73 +408,44 @@ end
function emit_getproperty!(dag, node, options)
sym = make_symbol_safe(node.id)

ref_expr = :(r = Ref{$sym}(x))
conv_expr = :(ptr = Base.unsafe_convert(Ptr{$sym}, r))
fptr_expr = :(fptr = getproperty(ptr, f))

load_expr = :(GC.@preserve r unsafe_load(fptr))
load_expr.args[2] = nothing
# Build the macrocall manually so we can set the extra LineNumberNode to
# nothing to stop it from being printed.
return_expr = :(GC.@preserve r getproperty(ptr, f))
return_expr.args[2] = nothing

load_base_expr = :(GC.@preserve r unsafe_load(baseptr32))
load_base_expr.args[2] = nothing
load_next_expr = :(GC.@preserve r unsafe_load(baseptr32 + 4))
load_next_expr.args[2] = nothing

if is_bitfield_type(node.type)
ex = quote
if fptr isa Ptr
return $load_expr
else
baseptr, offset, width = fptr
ty = eltype(baseptr)
baseptr32 = convert(Ptr{UInt32}, baseptr)
u64 = $load_base_expr
if offset + width > 32
u64 |= ($load_next_expr) << 32
end
u64 = (u64 >> offset) & ((1 << width) - 1)
return u64 % ty
end
ex = quote
function Base.getproperty(x::$sym, f::Symbol)
r = Ref{$sym}(x)
ptr = Base.unsafe_convert(Ptr{$sym}, r)
return $return_expr
end
else
ex = load_expr
end

# Remove line number nodes and the enclosing :block node
rm_line_num_node!(ex)
ex = ex.args[1]

signature = Expr(:call, :(Base.getproperty), :(x::$sym), :(f::Symbol))
body = Expr(:block, ref_expr, conv_expr, fptr_expr, ex)
getproperty_expr = Expr(:function, signature, body)

push!(node.exprs, getproperty_expr)
push!(node.exprs, ex)

return dag
end

function emit_setproperty!(dag, node, options)
sym = make_symbol_safe(node.id)
signature = Expr(:call, :(Base.setproperty!), :(x::Ptr{$sym}), :(f::Symbol), :v)
store_expr = :(unsafe_store!(getproperty(x, f), v))

auto_deref = get(options, "auto_field_dereference", false)
pointer_getter = auto_deref ? :getptr : :getproperty
store_expr = :(unsafe_store!($pointer_getter(x, f), v))

if is_bitfield_type(node.type)
body = quote
fptr = getproperty(x, f)
fptr = $pointer_getter(x, f)
if fptr isa Ptr
$store_expr
else
baseptr, offset, width = fptr
baseptr32 = convert(Ptr{UInt32}, baseptr)
u64 = unsafe_load(baseptr32)
straddle = offset + width > 32
if straddle
u64 |= unsafe_load(baseptr32 + 4) << 32
end
mask = ((1 << width) - 1)
u64 &= ~(mask << offset)
u64 |= (unsigned(v) & mask) << offset
unsafe_store!(baseptr32, u64 & typemax(UInt32))
if straddle
unsafe_store!(baseptr32 + 4, u64 >> 32)
end
# setbitfieldproperty!() is emitted by ProloguePrinter
setbitfieldproperty!(fptr, v)
end
end
rm_line_num_node!(body)
Expand All @@ -431,7 +465,7 @@ function get_names_types(root_cursor, cursor, options)
for field_cursor in field_cursors
n = name(field_cursor)
if isempty(n)
_emit_getproperty_ptr!(root_cursor, field_cursor, options)
_emit_pointer_access!(root_cursor, field_cursor, options)
continue
end
fsym = make_symbol_safe(n)
Expand Down
70 changes: 70 additions & 0 deletions src/generator/passes.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1094,6 +1094,7 @@ function (x::ProloguePrinter)(dag::ExprDAG, options::Dict)
use_native_enum = get(general_options, "use_julia_native_enum_type", false)
print_CEnum = get(general_options, "print_using_CEnum", true)
wrap_variadic_function = get(codegen_options, "wrap_variadic_function", false)
auto_deref = get(codegen_options, "auto_field_dereference", false)

show_info && @info "[ProloguePrinter]: print to $(x.file)"
open(x.file, "w") do io
Expand Down Expand Up @@ -1132,6 +1133,75 @@ function (x::ProloguePrinter)(dag::ExprDAG, options::Dict)
""")
end

# Print the bitfield helpers if there are any bitfield structs
if any(is_bitfield_type(node.type) for node in dag.nodes)
# These expressions include macrocalls, which are oddly clingy to
# their LineNumberNode's (rm_line_num_node!() will give wrong
# results). Instead we create the expressions explicitly and remove
# the LineNumberNode's by setting their 2nd argument to nothing.
u64_expr = :(GC.@preserve obj_handle unsafe_load(baseptr32))
u64_expr.args[2] = nothing
ptrload_expr = :(GC.@preserve obj_handle unsafe_load(baseptr32 + 4))
ptrload_expr.args[2] = nothing

get_expr = quote
function getbitfieldproperty(obj_handle, bitfield_info)
baseptr, offset, width = bitfield_info
ty = eltype(baseptr)
baseptr32 = convert(Ptr{UInt32}, baseptr)
u64 = $u64_expr
if offset + width > 32
u64 |= ($ptrload_expr) << 32
end
u64 = (u64 >> offset) & ((1 << width) - 1)
return u64 % ty
end
end

set_expr = quote
function setbitfieldproperty!(bitfield_info, value)
baseptr, offset, width = bitfield_info
baseptr32 = convert(Ptr{UInt32}, baseptr)
u64 = unsafe_load(baseptr32)
straddle = offset + width > 32
if straddle
u64 |= unsafe_load(baseptr32 + 4) << 32
end
mask = ((1 << width) - 1)
u64 &= ~(mask << offset)
u64 |= (unsigned(value) & mask) << offset
unsafe_store!(baseptr32, u64 & typemax(UInt32))
if straddle
unsafe_store!(baseptr32 + 4, u64 >> 32)
end
end
end

# Remove line number nodes and the extra :block node
rm_line_num_node!(get_expr)
rm_line_num_node!(set_expr)
get_expr = get_expr.args[1]
set_expr = set_expr.args[1]

println(io, string(get_expr), "\n")
println(io, string(set_expr), "\n")
end

if auto_deref
println(io, raw"""
macro ptr(expr)
if !Meta.isexpr(expr, :.)
error("Expression is not a property access, cannot use @ptr on it.")
end

quote
local penultimate_obj = $(esc(expr.args[1]))
getptr(penultimate_obj, $(esc(expr.args[2])))
end
end
""")
end

# print prelogue patches
if !isempty(prologue_file_path)
println(io, read(prologue_file_path, String))
Expand Down
Loading
Loading