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
10 changes: 10 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@

## v1.7.0

- Added `WrappedArray.emplace_struct`, to construct structs in place. This is more efficient than
calling `arr.insert(pos, unrealsdk.make_struct(...))`.

[dc515cdc](https://github.com/bl-sdk/unrealsdk/commit/dc515cdc)

- Added `unrealsdk.unreal.IGNORE_STRUCT`, a sentinel value which can be assigned to any struct, but
which does nothing. This is most useful when a function has a required struct arg.

[6c0b58ee](https://github.com/bl-sdk/unrealsdk/commit/6c0b58ee)

- Added support for sending property changed events. This is typically best done via the
`unrealsdk.unreal.notify_changes` context manager.

Expand Down
2 changes: 2 additions & 0 deletions src/pyunrealsdk/dllmain.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#include "pyunrealsdk/pch.h"
#include "pyunrealsdk/logging.h"
#include "pyunrealsdk/pyunrealsdk.h"

#ifdef PYUNREALSDK_INTERNAL
Expand All @@ -16,6 +17,7 @@ DWORD WINAPI startup_thread(LPVOID /*unused*/) {
pyunrealsdk::init();
} catch (std::exception& ex) {
LOG(ERROR, "Exception occurred while initializing the python sdk: {}", ex.what());
pyunrealsdk::logging::log_python_exception(ex);
}

return 1;
Expand Down
12 changes: 12 additions & 0 deletions src/pyunrealsdk/unreal_bindings/property_access.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
#include "pyunrealsdk/static_py_object.h"
#include "pyunrealsdk/unreal_bindings/uenum.h"
#include "pyunrealsdk/unreal_bindings/wrapped_array.h"
#include "pyunrealsdk/unreal_bindings/wrapped_struct.h"
#include "unrealsdk/unreal/cast.h"
#include "unrealsdk/unreal/classes/properties/uarrayproperty.h"
#include "unrealsdk/unreal/classes/properties/ustructproperty.h"
#include "unrealsdk/unreal/classes/uconst.h"
#include "unrealsdk/unreal/classes/uenum.h"
#include "unrealsdk/unreal/classes/ufield.h"
Expand Down Expand Up @@ -140,6 +142,9 @@ py::object py_getattr(UField* field,
field->Name, field->Class->Name));
}

// The templated lambda and all the if constexprs make everything have a really high penalty
// Yes it's probably a bit complex, but it's also a bit awkward trying to split it up
// NOLINTNEXTLINE(readability-function-cognitive-complexity)
void py_setattr_direct(UField* field, uintptr_t base_addr, const py::object& value) {
if (!field->is_instance(find_class<UProperty>())) {
throw py::attribute_error(unrealsdk::fmt::format(
Expand Down Expand Up @@ -204,6 +209,13 @@ void py_setattr_direct(UField* field, uintptr_t base_addr, const py::object& val
}

for (size_t i = 0; i < seq_size; i++) {
// If we're setting a struct property, we might be being told to ignore it
if constexpr (std::is_base_of_v<UStructProperty, T>) {
if (is_ignore_struct_sentinel(value_seq[i])) {
continue;
}
}

set_property<T>(prop, i, base_addr, py::cast<value_type>(value_seq[i]));
}
});
Expand Down
13 changes: 13 additions & 0 deletions src/pyunrealsdk/unreal_bindings/wrapped_array.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,19 @@ void register_wrapped_array(py::module_& mod) {
"\n"
"Returns:\n"
" This array's address.")
.def("emplace_struct", &impl::array_py_emplace_struct,
"If this is an array of structs, inserts a new struct in place.\n"
"\n"
"This avoids the extra allocations caused by calling unrealsdk.make_struct().\n"
"\n"
"Throws a TypeError if this is another type of array.\n"
"\n"
"Args:\n"
" idx: The index to insert before. Defaults to the end of the array.\n"
" *args: Fields on the struct to initialize. Note you must explicitly specify\n"
" idx to use these.\n"
" **kwargs: Fields on the struct to initialize.",
"idx"_a = std::numeric_limits<py::ssize_t>::max(), py::pos_only{})
.def_readwrite("_type", &WrappedArray::type);

// Create as a class method, see pybind11#1693
Expand Down
5 changes: 5 additions & 0 deletions src/pyunrealsdk/unreal_bindings/wrapped_array.h
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,11 @@ void array_py_reverse(WrappedArray& self);
void array_py_sort(WrappedArray& self, const py::object& key, bool reverse);
// _get_address
uintptr_t array_py_getaddress(const WrappedArray& self);
// emplace_struct
void array_py_emplace_struct(WrappedArray& self,
py::ssize_t py_idx,
const py::args& args,
const py::kwargs& kwargs);

} // namespace impl

Expand Down
58 changes: 58 additions & 0 deletions src/pyunrealsdk/unreal_bindings/wrapped_array_methods.cpp
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
#include "pyunrealsdk/pch.h"
#include "pyunrealsdk/static_py_object.h"
#include "pyunrealsdk/unreal_bindings/wrapped_array.h"
#include "pyunrealsdk/unreal_bindings/wrapped_struct.h"
#include "unrealsdk/unreal/cast.h"
#include "unrealsdk/unreal/classes/properties/ustructproperty.h"
#include "unrealsdk/unreal/find_class.h"
#include "unrealsdk/unreal/wrappers/wrapped_array.h"
#include "unrealsdk/unreal/wrappers/wrapped_struct.h"

#ifdef PYUNREALSDK_INTERNAL

Expand Down Expand Up @@ -179,6 +183,60 @@ uintptr_t array_py_getaddress(const WrappedArray& self) {
return reinterpret_cast<uintptr_t>(self.base.get());
}

void array_py_emplace_struct(WrappedArray& self,
py::ssize_t py_idx,
const py::args& args,
const py::kwargs& kwargs) {
if (!self.type->is_instance(find_class<UStructProperty>())) {
throw py::type_error("tried to emplace_struct into an array not made of structs");
}

auto size = self.size();

if (static_cast<size_t>(py_idx) >= size) {
// We're just appending
self.resize(size + 1);
try {
auto new_struct = self.get_at<UStructProperty>(size);
// May need to zero if there's still leftovers from when this array was bigger
memset(new_struct.base.get(), 0, new_struct.type->get_struct_size());
make_struct(new_struct, args, kwargs);
} catch (...) {
self.resize(size);
throw;
}
return;
}

// Copied from insert, shift all elements to make space for the one we're inserting
auto idx = convert_py_idx(self, py_idx);

self.resize(size + 1);

auto data = reinterpret_cast<uintptr_t>(self.base->data);
auto element_size = self.type->ElementSize;

auto src = data + (idx * element_size);
auto remaining_size = (size - idx) * element_size;
memmove(reinterpret_cast<void*>(src + element_size), reinterpret_cast<void*>(src),
remaining_size);

try {
auto new_struct = self.get_at<UStructProperty>(idx);
// At this point the struct still has all it's old contents. We don't need to properly
// destroy them since we've just moved it to the next slot, we're not leaking anything.
// Definitely need to zero it though.
memset(new_struct.base.get(), 0, new_struct.type->get_struct_size());
make_struct(new_struct, args, kwargs);
} catch (...) {
// Move it all back
memmove(reinterpret_cast<void*>(src), reinterpret_cast<void*>(src + element_size),
remaining_size);
self.resize(size);
throw;
}
}

} // namespace pyunrealsdk::unreal::impl

#endif
47 changes: 39 additions & 8 deletions src/pyunrealsdk/unreal_bindings/wrapped_struct.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,23 @@ using namespace unrealsdk::unreal;

namespace pyunrealsdk::unreal {

namespace {

/**
* @brief Gets the ignore struct sentinel.
*
* @return The ignore struct sentinel.
*/
py::object get_ignore_struct_sentinel(void) {
PYBIND11_CONSTINIT static py::gil_safe_call_once_and_store<py::object> storage;
return storage
.call_once_and_store_result(
[]() { return py::module_::import("builtins").attr("object")(); })
.get_stored();
}

} // namespace

WrappedStruct make_struct(
std::variant<const unrealsdk::unreal::UFunction*, const unrealsdk::unreal::UScriptStruct*> type,
const py::args& args,
Expand All @@ -27,7 +44,14 @@ WrappedStruct make_struct(
std::visit([&struct_type](auto&& val) { struct_type = val; }, type);

WrappedStruct new_struct{struct_type};
make_struct(new_struct, args, kwargs);

return new_struct;
}

void make_struct(unrealsdk::unreal::WrappedStruct& out_struct,
const py::args& args,
const py::kwargs& kwargs) {
// Convert the kwarg keys to FNames, to make them case insensitive
// This should also in theory speed up lookups, since hashing is simpler
std::unordered_map<FName, py::object> converted_kwargs{};
Expand All @@ -38,15 +62,15 @@ WrappedStruct make_struct(
});

size_t arg_idx = 0;
for (auto prop : struct_type->properties()) {
for (auto prop : out_struct.type->properties()) {
if (arg_idx != args.size()) {
py_setattr_direct(prop, reinterpret_cast<uintptr_t>(new_struct.base.get()),
py_setattr_direct(prop, reinterpret_cast<uintptr_t>(out_struct.base.get()),
args[arg_idx++]);

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

continue;
Expand All @@ -56,7 +80,7 @@ WrappedStruct make_struct(
auto iter = converted_kwargs.find(prop->Name);
if (iter != converted_kwargs.end()) {
// Use extract to also remove the value from the map, so we can ensure it's empty later
py_setattr_direct(prop, reinterpret_cast<uintptr_t>(new_struct.base.get()),
py_setattr_direct(prop, reinterpret_cast<uintptr_t>(out_struct.base.get()),
converted_kwargs.extract(iter).mapped());
continue;
}
Expand All @@ -66,15 +90,16 @@ WrappedStruct make_struct(
// Copying python, we only need to warn about one extra kwarg
throw py::type_error(
unrealsdk::fmt::format("{}.__init__() got an unexpected keyword argument '{}'",
struct_type->Name, converted_kwargs.begin()->first));
out_struct.type->Name, converted_kwargs.begin()->first));
}

return new_struct;
}

void register_wrapped_struct(py::module_& mod) {
py::class_<WrappedStruct>(mod, "WrappedStruct")
.def(py::init(&make_struct),
.def(py::init([](std::variant<const unrealsdk::unreal::UFunction*,
const unrealsdk::unreal::UScriptStruct*> type,
const py::args& args,
const py::kwargs& kwargs) { return make_struct(type, args, kwargs); }),
"Creates a new wrapped struct.\n"
"\n"
"Args:\n"
Expand Down Expand Up @@ -236,6 +261,12 @@ void register_wrapped_struct(py::module_& mod) {
"Returns:\n"
" This struct's address.")
.def_readwrite("_type", &WrappedStruct::type);

mod.attr("IGNORE_STRUCT") = get_ignore_struct_sentinel();
}

bool is_ignore_struct_sentinel(const py::object& obj) {
return obj.is(get_ignore_struct_sentinel());
}

} // namespace pyunrealsdk::unreal
Expand Down
12 changes: 12 additions & 0 deletions src/pyunrealsdk/unreal_bindings/wrapped_struct.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ void register_wrapped_struct(py::module_& mod);
* @brief Creates a new wrapped struct using python args.
*
* @param type The type of struct to make.
* @param out_struct The existing struct to write into.
* @param args The python args.
* @param kwargs The python kwargs.
* @return The new wrapped struct.
Expand All @@ -34,6 +35,17 @@ unrealsdk::unreal::WrappedStruct make_struct(
std::variant<const unrealsdk::unreal::UFunction*, const unrealsdk::unreal::UScriptStruct*> type,
const py::args& args,
const py::kwargs& kwargs);
void make_struct(unrealsdk::unreal::WrappedStruct& out_struct,
const py::args& args,
const py::kwargs& kwargs);

/**
* @brief Checks if a python object is the ignore struct sentinel.
*
* @param obj The object to check.
* @return True if the object is the ignore struct sentinel.
*/
bool is_ignore_struct_sentinel(const py::object& obj);

} // namespace pyunrealsdk::unreal

Expand Down
3 changes: 2 additions & 1 deletion stubs/unrealsdk/unreal/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,10 @@ from ._uobject_children import (
from ._weak_pointer import WeakPointer
from ._wrapped_array import WrappedArray
from ._wrapped_multicast_delegate import WrappedMulticastDelegate
from ._wrapped_struct import WrappedStruct
from ._wrapped_struct import IGNORE_STRUCT, WrappedStruct

__all__: tuple[str, ...] = (
"IGNORE_STRUCT",
"BoundFunction",
"UArrayProperty",
"UBlueprintGeneratedClass",
Expand Down
14 changes: 14 additions & 0 deletions stubs/unrealsdk/unreal/_wrapped_array.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,20 @@ class WrappedArray[T]:
Returns:
The number of times the value appears in the array.
"""
def emplace_struct(self, idx: int = sys.maxsize, /, *args: Any, **kwargs: Any) -> None:
"""
If this is an array of structs, inserts a new struct in place.

This avoids the extra allocations caused by calling unrealsdk.make_struct().

Throws a TypeError if this is another type of array.

Args:
idx: The index to insert before. Defaults to the end of the array.
*args: Fields on the struct to initialize. Note you must explicitly specify
idx to use these.
**kwargs: Fields on the struct to initialize.
"""
def extend(self, values: Sequence[T]) -> None:
"""
Extends the array with all the values in the given sequence.
Expand Down
5 changes: 5 additions & 0 deletions stubs/unrealsdk/unreal/_wrapped_struct.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ from typing import Any

from ._uobject_children import UField, UFunction, UScriptStruct, UStruct

# A sentinel value which can be assigned to any struct property, but which does nothing.
# This is most useful when a function has a required struct arg, but you want to use the default,
# zero-init, value.
IGNORE_STRUCT: object

class WrappedStruct:
_type: UStruct

Expand Down