Skip to content

Commit 9cb88b5

Browse files
authored
Merge pull request #72 from apple1417/master
add some wrapped struct qol helpers
2 parents a0d8fdb + 4f6b236 commit 9cb88b5

11 files changed

+172
-9
lines changed

changelog.md

+10
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,16 @@
22

33
## v1.7.0
44

5+
- Added `WrappedArray.emplace_struct`, to construct structs in place. This is more efficient than
6+
calling `arr.insert(pos, unrealsdk.make_struct(...))`.
7+
8+
[dc515cdc](https://github.com/bl-sdk/unrealsdk/commit/dc515cdc)
9+
10+
- Added `unrealsdk.unreal.IGNORE_STRUCT`, a sentinel value which can be assigned to any struct, but
11+
which does nothing. This is most useful when a function has a required struct arg.
12+
13+
[6c0b58ee](https://github.com/bl-sdk/unrealsdk/commit/6c0b58ee)
14+
515
- Added support for sending property changed events. This is typically best done via the
616
`unrealsdk.unreal.notify_changes` context manager.
717

src/pyunrealsdk/dllmain.cpp

+2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#include "pyunrealsdk/pch.h"
2+
#include "pyunrealsdk/logging.h"
23
#include "pyunrealsdk/pyunrealsdk.h"
34

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

2123
return 1;

src/pyunrealsdk/unreal_bindings/property_access.cpp

+12
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33
#include "pyunrealsdk/static_py_object.h"
44
#include "pyunrealsdk/unreal_bindings/uenum.h"
55
#include "pyunrealsdk/unreal_bindings/wrapped_array.h"
6+
#include "pyunrealsdk/unreal_bindings/wrapped_struct.h"
67
#include "unrealsdk/unreal/cast.h"
78
#include "unrealsdk/unreal/classes/properties/uarrayproperty.h"
9+
#include "unrealsdk/unreal/classes/properties/ustructproperty.h"
810
#include "unrealsdk/unreal/classes/uconst.h"
911
#include "unrealsdk/unreal/classes/uenum.h"
1012
#include "unrealsdk/unreal/classes/ufield.h"
@@ -140,6 +142,9 @@ py::object py_getattr(UField* field,
140142
field->Name, field->Class->Name));
141143
}
142144

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

206211
for (size_t i = 0; i < seq_size; i++) {
212+
// If we're setting a struct property, we might be being told to ignore it
213+
if constexpr (std::is_base_of_v<UStructProperty, T>) {
214+
if (is_ignore_struct_sentinel(value_seq[i])) {
215+
continue;
216+
}
217+
}
218+
207219
set_property<T>(prop, i, base_addr, py::cast<value_type>(value_seq[i]));
208220
}
209221
});

src/pyunrealsdk/unreal_bindings/wrapped_array.cpp

+13
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,19 @@ void register_wrapped_array(py::module_& mod) {
194194
"\n"
195195
"Returns:\n"
196196
" This array's address.")
197+
.def("emplace_struct", &impl::array_py_emplace_struct,
198+
"If this is an array of structs, inserts a new struct in place.\n"
199+
"\n"
200+
"This avoids the extra allocations caused by calling unrealsdk.make_struct().\n"
201+
"\n"
202+
"Throws a TypeError if this is another type of array.\n"
203+
"\n"
204+
"Args:\n"
205+
" idx: The index to insert before. Defaults to the end of the array.\n"
206+
" *args: Fields on the struct to initialize. Note you must explicitly specify\n"
207+
" idx to use these.\n"
208+
" **kwargs: Fields on the struct to initialize.",
209+
"idx"_a = std::numeric_limits<py::ssize_t>::max(), py::pos_only{})
197210
.def_readwrite("_type", &WrappedArray::type);
198211

199212
// Create as a class method, see pybind11#1693

src/pyunrealsdk/unreal_bindings/wrapped_array.h

+5
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,11 @@ void array_py_reverse(WrappedArray& self);
199199
void array_py_sort(WrappedArray& self, const py::object& key, bool reverse);
200200
// _get_address
201201
uintptr_t array_py_getaddress(const WrappedArray& self);
202+
// emplace_struct
203+
void array_py_emplace_struct(WrappedArray& self,
204+
py::ssize_t py_idx,
205+
const py::args& args,
206+
const py::kwargs& kwargs);
202207

203208
} // namespace impl
204209

src/pyunrealsdk/unreal_bindings/wrapped_array_methods.cpp

+58
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
#include "pyunrealsdk/pch.h"
22
#include "pyunrealsdk/static_py_object.h"
33
#include "pyunrealsdk/unreal_bindings/wrapped_array.h"
4+
#include "pyunrealsdk/unreal_bindings/wrapped_struct.h"
45
#include "unrealsdk/unreal/cast.h"
6+
#include "unrealsdk/unreal/classes/properties/ustructproperty.h"
7+
#include "unrealsdk/unreal/find_class.h"
58
#include "unrealsdk/unreal/wrappers/wrapped_array.h"
9+
#include "unrealsdk/unreal/wrappers/wrapped_struct.h"
610

711
#ifdef PYUNREALSDK_INTERNAL
812

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

186+
void array_py_emplace_struct(WrappedArray& self,
187+
py::ssize_t py_idx,
188+
const py::args& args,
189+
const py::kwargs& kwargs) {
190+
if (!self.type->is_instance(find_class<UStructProperty>())) {
191+
throw py::type_error("tried to emplace_struct into an array not made of structs");
192+
}
193+
194+
auto size = self.size();
195+
196+
if (static_cast<size_t>(py_idx) >= size) {
197+
// We're just appending
198+
self.resize(size + 1);
199+
try {
200+
auto new_struct = self.get_at<UStructProperty>(size);
201+
// May need to zero if there's still leftovers from when this array was bigger
202+
memset(new_struct.base.get(), 0, new_struct.type->get_struct_size());
203+
make_struct(new_struct, args, kwargs);
204+
} catch (...) {
205+
self.resize(size);
206+
throw;
207+
}
208+
return;
209+
}
210+
211+
// Copied from insert, shift all elements to make space for the one we're inserting
212+
auto idx = convert_py_idx(self, py_idx);
213+
214+
self.resize(size + 1);
215+
216+
auto data = reinterpret_cast<uintptr_t>(self.base->data);
217+
auto element_size = self.type->ElementSize;
218+
219+
auto src = data + (idx * element_size);
220+
auto remaining_size = (size - idx) * element_size;
221+
memmove(reinterpret_cast<void*>(src + element_size), reinterpret_cast<void*>(src),
222+
remaining_size);
223+
224+
try {
225+
auto new_struct = self.get_at<UStructProperty>(idx);
226+
// At this point the struct still has all it's old contents. We don't need to properly
227+
// destroy them since we've just moved it to the next slot, we're not leaking anything.
228+
// Definitely need to zero it though.
229+
memset(new_struct.base.get(), 0, new_struct.type->get_struct_size());
230+
make_struct(new_struct, args, kwargs);
231+
} catch (...) {
232+
// Move it all back
233+
memmove(reinterpret_cast<void*>(src), reinterpret_cast<void*>(src + element_size),
234+
remaining_size);
235+
self.resize(size);
236+
throw;
237+
}
238+
}
239+
182240
} // namespace pyunrealsdk::unreal::impl
183241

184242
#endif

src/pyunrealsdk/unreal_bindings/wrapped_struct.cpp

+39-8
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,23 @@ using namespace unrealsdk::unreal;
1616

1717
namespace pyunrealsdk::unreal {
1818

19+
namespace {
20+
21+
/**
22+
* @brief Gets the ignore struct sentinel.
23+
*
24+
* @return The ignore struct sentinel.
25+
*/
26+
py::object get_ignore_struct_sentinel(void) {
27+
PYBIND11_CONSTINIT static py::gil_safe_call_once_and_store<py::object> storage;
28+
return storage
29+
.call_once_and_store_result(
30+
[]() { return py::module_::import("builtins").attr("object")(); })
31+
.get_stored();
32+
}
33+
34+
} // namespace
35+
1936
WrappedStruct make_struct(
2037
std::variant<const unrealsdk::unreal::UFunction*, const unrealsdk::unreal::UScriptStruct*> type,
2138
const py::args& args,
@@ -27,7 +44,14 @@ WrappedStruct make_struct(
2744
std::visit([&struct_type](auto&& val) { struct_type = val; }, type);
2845

2946
WrappedStruct new_struct{struct_type};
47+
make_struct(new_struct, args, kwargs);
48+
49+
return new_struct;
50+
}
3051

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

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

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

5276
continue;
@@ -56,7 +80,7 @@ WrappedStruct make_struct(
5680
auto iter = converted_kwargs.find(prop->Name);
5781
if (iter != converted_kwargs.end()) {
5882
// Use extract to also remove the value from the map, so we can ensure it's empty later
59-
py_setattr_direct(prop, reinterpret_cast<uintptr_t>(new_struct.base.get()),
83+
py_setattr_direct(prop, reinterpret_cast<uintptr_t>(out_struct.base.get()),
6084
converted_kwargs.extract(iter).mapped());
6185
continue;
6286
}
@@ -66,15 +90,16 @@ WrappedStruct make_struct(
6690
// Copying python, we only need to warn about one extra kwarg
6791
throw py::type_error(
6892
unrealsdk::fmt::format("{}.__init__() got an unexpected keyword argument '{}'",
69-
struct_type->Name, converted_kwargs.begin()->first));
93+
out_struct.type->Name, converted_kwargs.begin()->first));
7094
}
71-
72-
return new_struct;
7395
}
7496

7597
void register_wrapped_struct(py::module_& mod) {
7698
py::class_<WrappedStruct>(mod, "WrappedStruct")
77-
.def(py::init(&make_struct),
99+
.def(py::init([](std::variant<const unrealsdk::unreal::UFunction*,
100+
const unrealsdk::unreal::UScriptStruct*> type,
101+
const py::args& args,
102+
const py::kwargs& kwargs) { return make_struct(type, args, kwargs); }),
78103
"Creates a new wrapped struct.\n"
79104
"\n"
80105
"Args:\n"
@@ -236,6 +261,12 @@ void register_wrapped_struct(py::module_& mod) {
236261
"Returns:\n"
237262
" This struct's address.")
238263
.def_readwrite("_type", &WrappedStruct::type);
264+
265+
mod.attr("IGNORE_STRUCT") = get_ignore_struct_sentinel();
266+
}
267+
268+
bool is_ignore_struct_sentinel(const py::object& obj) {
269+
return obj.is(get_ignore_struct_sentinel());
239270
}
240271

241272
} // namespace pyunrealsdk::unreal

src/pyunrealsdk/unreal_bindings/wrapped_struct.h

+12
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ void register_wrapped_struct(py::module_& mod);
2626
* @brief Creates a new wrapped struct using python args.
2727
*
2828
* @param type The type of struct to make.
29+
* @param out_struct The existing struct to write into.
2930
* @param args The python args.
3031
* @param kwargs The python kwargs.
3132
* @return The new wrapped struct.
@@ -34,6 +35,17 @@ unrealsdk::unreal::WrappedStruct make_struct(
3435
std::variant<const unrealsdk::unreal::UFunction*, const unrealsdk::unreal::UScriptStruct*> type,
3536
const py::args& args,
3637
const py::kwargs& kwargs);
38+
void make_struct(unrealsdk::unreal::WrappedStruct& out_struct,
39+
const py::args& args,
40+
const py::kwargs& kwargs);
41+
42+
/**
43+
* @brief Checks if a python object is the ignore struct sentinel.
44+
*
45+
* @param obj The object to check.
46+
* @return True if the object is the ignore struct sentinel.
47+
*/
48+
bool is_ignore_struct_sentinel(const py::object& obj);
3749

3850
} // namespace pyunrealsdk::unreal
3951

stubs/unrealsdk/unreal/__init__.pyi

+2-1
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,10 @@ from ._uobject_children import (
4848
from ._weak_pointer import WeakPointer
4949
from ._wrapped_array import WrappedArray
5050
from ._wrapped_multicast_delegate import WrappedMulticastDelegate
51-
from ._wrapped_struct import WrappedStruct
51+
from ._wrapped_struct import IGNORE_STRUCT, WrappedStruct
5252

5353
__all__: tuple[str, ...] = (
54+
"IGNORE_STRUCT",
5455
"BoundFunction",
5556
"UArrayProperty",
5657
"UBlueprintGeneratedClass",

stubs/unrealsdk/unreal/_wrapped_array.pyi

+14
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,20 @@ class WrappedArray[T]:
189189
Returns:
190190
The number of times the value appears in the array.
191191
"""
192+
def emplace_struct(self, idx: int = sys.maxsize, /, *args: Any, **kwargs: Any) -> None:
193+
"""
194+
If this is an array of structs, inserts a new struct in place.
195+
196+
This avoids the extra allocations caused by calling unrealsdk.make_struct().
197+
198+
Throws a TypeError if this is another type of array.
199+
200+
Args:
201+
idx: The index to insert before. Defaults to the end of the array.
202+
*args: Fields on the struct to initialize. Note you must explicitly specify
203+
idx to use these.
204+
**kwargs: Fields on the struct to initialize.
205+
"""
192206
def extend(self, values: Sequence[T]) -> None:
193207
"""
194208
Extends the array with all the values in the given sequence.

stubs/unrealsdk/unreal/_wrapped_struct.pyi

+5
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ from typing import Any
44

55
from ._uobject_children import UField, UFunction, UScriptStruct, UStruct
66

7+
# A sentinel value which can be assigned to any struct property, but which does nothing.
8+
# This is most useful when a function has a required struct arg, but you want to use the default,
9+
# zero-init, value.
10+
IGNORE_STRUCT: object
11+
712
class WrappedStruct:
813
_type: UStruct
914

0 commit comments

Comments
 (0)