Skip to content

Commit

Permalink
Merge pull request #47 from apple1417/master
Browse files Browse the repository at this point in the history
support delegate properties
  • Loading branch information
apple1417 authored Sep 22, 2024
2 parents 64b6d74 + 2c6dfa4 commit 608a2ea
Show file tree
Hide file tree
Showing 20 changed files with 567 additions and 98 deletions.
15 changes: 15 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
# Changelog

## Upcoming

- Fixed weak pointer type hinting to allow for null pointers. This always worked at runtime.

[1cbded47](https://github.com/bl-sdk/pyunrealsdk/commit/1cbded47)

- Added support for Delegate and Multicast Delegate properties.

[04d47f92](https://github.com/bl-sdk/pyunrealsdk/commit/04d47f92),
[2876f098](https://github.com/bl-sdk/pyunrealsdk/commit/2876f098)

- Added a repr to `BoundFunction`, as these are now returned by delegates.

[22082579](https://github.com/bl-sdk/pyunrealsdk/commit/22082579)

## v1.3.0

Also see the unrealsdk v1.3.0 changelog [here](https://github.com/bl-sdk/unrealsdk/blob/master/changelog.md#v130).
Expand Down
2 changes: 1 addition & 1 deletion src/pyunrealsdk/dllmain.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ DWORD WINAPI startup_thread(LPVOID /*unused*/) {
* @param ul_reason_for_call Reason this is being called.
* @return True if loaded successfully, false otherwise.
*/
// NOLINTNEXTLINE(readability-identifier-naming) - for `DllMain`
// NOLINTNEXTLINE(misc-use-internal-linkage, readability-identifier-naming)
BOOL APIENTRY DllMain(HMODULE h_module, DWORD ul_reason_for_call, LPVOID /*unused*/) {
switch (ul_reason_for_call) {
case DLL_PROCESS_ATTACH:
Expand Down
3 changes: 2 additions & 1 deletion src/pyunrealsdk/logging.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#include "pyunrealsdk/pch.h"
#include "unrealsdk/logging.h"
#include "pyunrealsdk/logging.h"
#include "unrealsdk/format.h"
#include "unrealsdk/logging.h"
#include "unrealsdk/unrealsdk.h"

using unrealsdk::logging::Level;
Expand Down
1 change: 1 addition & 0 deletions src/pyunrealsdk/pyunrealsdk.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#include "pyunrealsdk/pch.h"
#include "pyunrealsdk/pyunrealsdk.h"
#include "pyunrealsdk/base_bindings.h"
#include "pyunrealsdk/commands.h"
#include "pyunrealsdk/env.h"
Expand Down
2 changes: 2 additions & 0 deletions src/pyunrealsdk/unreal_bindings/bindings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include "pyunrealsdk/unreal_bindings/uobject_children.h"
#include "pyunrealsdk/unreal_bindings/weak_pointer.h"
#include "pyunrealsdk/unreal_bindings/wrapped_array.h"
#include "pyunrealsdk/unreal_bindings/wrapped_multicast_delegate.h"
#include "pyunrealsdk/unreal_bindings/wrapped_struct.h"
#include "unrealsdk/unreal/classes/ufield.h"
#include "unrealsdk/unreal/classes/ustruct.h"
Expand All @@ -32,6 +33,7 @@ void register_module(py::module_& mod) {
register_bound_function(unreal);
register_weak_pointer(unreal);
register_persistent_object_properties(unreal);
register_wrapped_multicast_delegate(unreal);
}

} // namespace pyunrealsdk::unreal
Expand Down
165 changes: 82 additions & 83 deletions src/pyunrealsdk/unreal_bindings/bound_function.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -52,42 +52,37 @@ namespace {
* @note While this is similar to `make_struct`, we need to do some extra processing on the params,
* and we need to fail if an arg was missed.
*
* @param params The params struct to fill.
* @param info The call info to write to.
* @param args The python args.
* @param kwargs The python kwargs.
* @return A pair of the return param (may be nullptr), and any out params (may be empty), to be
* passed to `get_py_return`.
*/
std::pair<UProperty*, std::vector<UProperty*>> fill_py_params(WrappedStruct& params,
const py::args& args,
const py::kwargs& kwargs) {
UProperty* return_param = nullptr;
std::vector<UProperty*> out_params{};

void fill_py_params(impl::PyCallInfo& info, const py::args& args, const py::kwargs& kwargs) {
size_t arg_idx = 0;

std::vector<FName> missing_required_args{};

for (auto prop : params.type->properties()) {
for (auto prop : info.params.type->properties()) {
if ((prop->PropertyFlags & UProperty::PROP_FLAG_PARAM) == 0) {
continue;
}
if ((prop->PropertyFlags & UProperty::PROP_FLAG_RETURN) != 0 && return_param == nullptr) {
return_param = prop;
if ((prop->PropertyFlags & UProperty::PROP_FLAG_RETURN) != 0
&& info.return_param == nullptr) {
info.return_param = prop;
continue;
}
if ((prop->PropertyFlags & UProperty::PROP_FLAG_OUT) != 0) {
out_params.push_back(prop);
info.out_params.push_back(prop);
}

// If we still have positional args left
if (arg_idx != args.size()) {
py_setattr_direct(prop, reinterpret_cast<uintptr_t>(params.base.get()),
py_setattr_direct(prop, reinterpret_cast<uintptr_t>(info.params.base.get()),
args[arg_idx++]);

if (kwargs.contains(prop->Name)) {
throw py::type_error(unrealsdk::fmt::format(
"{}() got multiple values for argument '{}'", params.type->Name, prop->Name));
throw py::type_error(
unrealsdk::fmt::format("{}() got multiple values for argument '{}'",
info.params.type->Name, prop->Name));
}

continue;
Expand All @@ -97,7 +92,7 @@ std::pair<UProperty*, std::vector<UProperty*>> fill_py_params(WrappedStruct& par
if (kwargs.contains(prop->Name)) {
// Extract the value with pop, so we can check that kwargs are empty at the
// end
py_setattr_direct(prop, reinterpret_cast<uintptr_t>(params.base.get()),
py_setattr_direct(prop, reinterpret_cast<uintptr_t>(info.params.base.get()),
kwargs.attr("pop")(prop->Name));
continue;
}
Expand All @@ -115,69 +110,81 @@ std::pair<UProperty*, std::vector<UProperty*>> fill_py_params(WrappedStruct& par
}

if (!missing_required_args.empty()) {
throw_missing_required_args(params.type->Name, missing_required_args);
throw_missing_required_args(info.params.type->Name, missing_required_args);
}

if (!kwargs.empty()) {
// Copying python, we only need to warn about one extra kwarg
std::string bad_kwarg = py::str(kwargs.begin()->first);
throw py::type_error(unrealsdk::fmt::format("{}() got an unexpected keyword argument '{}'",
params.type->Name, bad_kwarg));
info.params.type->Name, bad_kwarg));
}
}

} // namespace

namespace impl {

PyCallInfo::PyCallInfo(const UFunction* func, const py::args& args, const py::kwargs& kwargs)
// Start by initializing a null struct, to avoid allocations
: params(func, nullptr) {
if (func->NumParams < args.size()) {
throw py::type_error(
unrealsdk::fmt::format("{}() takes {} positional args, but {} were given", func->Name,
func->NumParams, args.size()));
}

// If we're given exactly one arg, and it's a wrapped struct of our function type, take it as
// the args directly
if (args.size() == 1 && kwargs.empty() && py::isinstance<WrappedStruct>(args[0])) {
auto args_struct = py::cast<WrappedStruct>(args[0]);
if (args_struct.type == func) {
this->params = std::move(args_struct);

// Manually gather the return value and out params
for (auto prop : func->properties()) {
if ((prop->PropertyFlags & UProperty::PROP_FLAG_RETURN) != 0
&& return_param == nullptr) {
this->return_param = prop;
continue;
}
if ((prop->PropertyFlags & UProperty::PROP_FLAG_OUT) != 0) {
this->out_params.push_back(prop);
}
}
return;
}
}

return {return_param, out_params};
// Otherwise, allocate a new params struct
this->params = WrappedStruct{func};
fill_py_params(*this, args, kwargs);
}

/**
* @brief Get the python return value for a function call.
*
* @param params The params struct to read the value out of.
* @param return_param The return param.
* @param out_params A list of the out params.
* @return The value to return to python.
*/
py::object get_py_return(const WrappedStruct& params,
UProperty* return_param,
const std::vector<UProperty*>& out_params) {
// NOLINTNEXTLINE(misc-const-correctness)
py::list ret{1 + out_params.size()};
py::object PyCallInfo::get_py_return(void) const {
const py::list ret{1 + this->out_params.size()};

if (return_param == nullptr) {
if (this->return_param == nullptr) {
ret[0] = py::ellipsis{};
} else {
ret[0] =
py_getattr(return_param, reinterpret_cast<uintptr_t>(params.base.get()), params.base);
py_getattr(this->return_param, reinterpret_cast<uintptr_t>(this->params.base.get()),
this->params.base);
}

auto idx = 1;
for (auto prop : out_params) {
ret[idx++] = py_getattr(prop, reinterpret_cast<uintptr_t>(params.base.get()), params.base);
for (auto prop : this->out_params) {
ret[idx++] = py_getattr(prop, reinterpret_cast<uintptr_t>(this->params.base.get()),
this->params.base);
}

if (out_params.empty()) {
if (this->out_params.empty()) {
return ret[0];
}
return py::tuple(ret);
}
py::object get_py_return(const WrappedStruct& params) {
// If only called with the struct, re-gather the return + out params
UProperty* return_param = nullptr;
std::vector<UProperty*> out_params{};

for (auto prop : params.type->properties()) {
if ((prop->PropertyFlags & UProperty::PROP_FLAG_RETURN) != 0 && return_param == nullptr) {
return_param = prop;
continue;
}
if ((prop->PropertyFlags & UProperty::PROP_FLAG_OUT) != 0) {
out_params.push_back(prop);
}
}

return get_py_return(params, return_param, out_params);
}

} // namespace
} // namespace impl

void register_bound_function(py::module_& mod) {
py::class_<BoundFunction>(mod, "BoundFunction")
Expand All @@ -188,41 +195,33 @@ void register_bound_function(py::module_& mod) {
" func: The function to bind.\n"
" object: The object the function is bound to.",
"func"_a, "object"_a)
.def(
"__repr__",
[](BoundFunction& self) {
return unrealsdk::fmt::format(
"<bound function {} on {}>", self.func->Name,
unrealsdk::utils::narrow(self.object->get_path_name()));
},
"Gets a string representation of this function and the object it's bound to.\n"
"\n"
"Returns:\n"
" The string representation.")
.def(
"__call__",
[](BoundFunction& self, const py::args& args, const py::kwargs& kwargs) {
if (self.func->NumParams < args.size()) {
throw py::type_error(
unrealsdk::fmt::format("{}() takes {} positional args, but {} were given",
self.func->Name, self.func->NumParams, args.size()));
}

if (args.size() == 1 && kwargs.empty() && py::isinstance<WrappedStruct>(args[0])) {
auto args_struct = py::cast<WrappedStruct>(args[0]);
if (args_struct.type == self.func) {
{
// Release the GIL to avoid a deadlock if ProcessEvent is locking.
// If a hook tries to call into Python, it will be holding the process
// event lock, and it will try to acquire the GIL.
// If at the same time python code on a different thread tries to call
// an unreal function, it would be holding the GIL, and trying to
// acquire the process event lock.
const py::gil_scoped_release gil{};
self.call<void>(args_struct);
}
return get_py_return(args_struct);
}
}

WrappedStruct params{self.func};
auto [return_param, out_params] = fill_py_params(params, args, kwargs);
impl::PyCallInfo info{self.func, args, kwargs};

// Release the GIL to avoid a deadlock if ProcessEvent is locking.
// If a hook tries to call into Python, it will be holding the process event lock,
// and it will try to acquire the GIL.
// If at the same time python code on a different thread tries to call an unreal
// function, it'd be holding the GIL, and trying to acquire the process event lock.
{
const py::gil_scoped_release gil{};
self.call<void>(params);
self.call<void>(info.params);
}

return get_py_return(params, return_param, out_params);
return info.get_py_return();
},
"Calls the function.\n"
"\n"
Expand Down
37 changes: 37 additions & 0 deletions src/pyunrealsdk/unreal_bindings/bound_function.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,48 @@
#define PYUNREALSDK_UNREAL_BINDINGS_BOUND_FUNCTION_H

#include "pyunrealsdk/pch.h"
#include "unrealsdk/unreal/wrappers/wrapped_struct.h"

#ifdef PYUNREALSDK_INTERNAL

namespace unrealsdk::unreal {

class UFunction;
class UProperty;

} // namespace unrealsdk::unreal

namespace pyunrealsdk::unreal {

namespace impl {

// Type helping convert a python function call to an unreal one.
struct PyCallInfo {
unrealsdk::unreal::WrappedStruct params;
unrealsdk::unreal::UProperty* return_param{};
std::vector<unrealsdk::unreal::UProperty*> out_params;

/**
* @brief Converts python args into a params struct.
*
* @param func The function being called.
* @param args The python args.
* @param kwargs The python kwargs.
*/
PyCallInfo(const unrealsdk::unreal::UFunction* func,
const py::args& args,
const py::kwargs& kwargs);

/**
* @brief Get the python return value for the function call from the contained params struct.
*
* @return The value to return to python.
*/
[[nodiscard]] py::object get_py_return(void) const;
};

} // namespace impl

/**
* @brief Registers BoundFunction.
*
Expand Down
7 changes: 4 additions & 3 deletions src/pyunrealsdk/unreal_bindings/property_access.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@ std::vector<std::string> py_dir(const py::object& self, const UStruct* type) {
if (dir_includes_unreal) {
// Append our fields
auto fields = type->fields();
std::transform(fields.begin(), fields.end(), std::back_inserter(names),
[](auto obj) { return obj->Name; });
std::ranges::transform(fields, std::back_inserter(names),
[](auto obj) { return obj->Name; });
}

return names;
Expand Down Expand Up @@ -120,7 +120,8 @@ py::object py_getattr(UField* field,
unrealsdk::fmt::format("cannot bind function '{}' with null object", field->Name));
}

return py::cast(BoundFunction{reinterpret_cast<UFunction*>(field), func_obj});
return py::cast(
BoundFunction{.func = reinterpret_cast<UFunction*>(field), .object = func_obj});
}

if (field->is_instance(find_class<UScriptStruct>())) {
Expand Down
Loading

0 comments on commit 608a2ea

Please sign in to comment.