Last active
October 7, 2023 15:16
-
-
Save dertom95/ceb071e196a5d571fe62 to your computer and use it in GitHub Desktop.
Blender: Access DNA-Datablocks from python
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# ##### BEGIN GPL LICENSE BLOCK ##### | |
# | |
# This program is free software; you can redistribute it and/or | |
# modify it under the terms of the GNU General Public License | |
# as published by the Free Software Foundation; either version 2 | |
# of the License, or (at your option) any later version. | |
# | |
# This program is distributed in the hope that it will be useful, | |
# but WITHOUT ANY WARRANTY; without even the implied warranty of | |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
# GNU General Public License for more details. | |
# | |
# You should have received a copy of the GNU General Public License | |
# along with this program; if not, write to the Free Software Foundation, | |
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |
# | |
# ##### END GPL LICENSE BLOCK ##### | |
# <pep8 compliant> | |
""" | |
Experimental module (for developers only), exposes DNA data via python | |
uses no blender/python modules, pure python + autogenerated ctypes api. | |
code by ideasman42 | |
(EDIT dertom: added bpy and setup to put _dna property to some types for instant dna-access) | |
Exposes: | |
* pydna.main: main database access. | |
* pydna.types: a module of all DNA ctypes structures. | |
* Utility method: CAST(dna_type) | |
* Utility method for ListBase: ITER(dna_type) | |
* Show structs: printStruct() or printStructs(["Image","Object"]) | |
Example: | |
import pydna | |
pydna.setup() # adds to _dna property to some types (see setup-def) | |
ob = bpy.data.objects[0] | |
print ("Name:" + ob._dna.id.name) | |
""" | |
import ctypes | |
import struct | |
import bpy | |
def _api(): | |
def is_ctypes_subclass(other, main_class): | |
while other: | |
if other is main_class: | |
return True | |
other = getattr(other, "_type_", None) | |
return False | |
def is_ctypes_initialized(other): | |
_other = other | |
while other: | |
if hasattr(other, "_fields_"): | |
return True | |
other = getattr(other, "_type_", None) | |
print(_other, "NOT INIT") | |
return False | |
def is_ctypes_base(other): | |
while type(getattr(other, "_type_", "")) != str: | |
other = other._type_ | |
return other | |
class MixIn: | |
pass | |
blend_cdll = ctypes.CDLL("") | |
blend_lib = ctypes.LibraryLoader("") | |
def blend_parse_dna(): | |
# from dna.c | |
sdna_str_pt = blend_cdll.DNAstr | |
sdna_len_pt = blend_cdll.DNAlen | |
# cast | |
sdna_len_pt = ctypes.c_void_p.from_address(ctypes.addressof(sdna_len_pt)) | |
sdna_len = ctypes.c_int.from_address(sdna_len_pt.value) | |
blend_sdna = ctypes.string_at(sdna_str_pt, sdna_len) | |
ofs = 0 | |
assert(blend_sdna[ofs:ofs + 8] == b'SDNANAME') | |
ofs += 8 | |
sdna_names_len = struct.unpack("i", blend_sdna[ofs:ofs + 4])[0] | |
ofs += 4 | |
blend_sdna_names = blend_sdna[ofs:].split(b'\0', sdna_names_len) | |
blend_sdna_remainder = blend_sdna_names.pop(-1) # last item is not a name. | |
ofs = len(blend_sdna) - len(blend_sdna_remainder) | |
ofs = (ofs + 3) & ~3 | |
assert(blend_sdna[ofs:ofs + 4] == b'TYPE') | |
ofs += 4 | |
sdna_types_len = struct.unpack("i", blend_sdna[ofs:ofs + 4])[0] | |
ofs += 4 | |
blend_sdna_types = blend_sdna[ofs:].split(b'\0', sdna_types_len) | |
blend_sdna_remainder = blend_sdna_types.pop(-1) | |
ofs = len(blend_sdna) - len(blend_sdna_remainder) | |
ofs = (ofs + 3) & ~3 | |
assert(blend_sdna[ofs:ofs + 4] == b'TLEN') | |
ofs += 4 | |
blend_sdna_typelens = struct.unpack("%dh" % sdna_types_len, blend_sdna[ofs:ofs + (sdna_types_len * 2)]) | |
ofs += sdna_types_len * 2 | |
ofs = (ofs + 3) & ~3 | |
# array of pointers to short arrays | |
assert(blend_sdna[ofs:ofs + 4] == b'STRC') | |
ofs += 4 | |
sdna_structs_len = struct.unpack("i", blend_sdna[ofs:ofs + 4])[0] | |
ofs += 4 | |
blend_sdna_structs = [] | |
for i in range(sdna_structs_len): | |
struct_type, struct_tot = struct.unpack("hh", blend_sdna[ofs:ofs + 4]) | |
ofs += 4 | |
struct_type_name_pairs = struct.unpack("%dh" % struct_tot * 2, blend_sdna[ofs:ofs + (struct_tot * 4)]) | |
# convert into pairs, easier to understand (type, name) | |
struct_type_name_pairs = [(struct_type_name_pairs[j], struct_type_name_pairs[j + 1]) for j in range(0, struct_tot * 2, 2)] | |
blend_sdna_structs.append((struct_type, struct_type_name_pairs)) | |
ofs += struct_tot * 4 | |
# ofs = (ofs + 1) & ~1 | |
return blend_sdna_names, blend_sdna_types, blend_sdna_typelens, blend_sdna_structs | |
def create_dna_structs(blend_sdna_names, blend_sdna_types, blend_sdna_typelens, blend_sdna_structs): | |
# create all subclasses of ctypes.Structure | |
ctypes_structs = {name: type(name.decode(), (ctypes.Structure, MixIn), {}) for name in blend_sdna_types} | |
ctypes_basic = {b"float": ctypes.c_float, b"double": ctypes.c_double, b"int": ctypes.c_int, b"short": ctypes.c_short, b"char": ctypes.c_char, b"void": ctypes.c_void_p} | |
ctypes_fields = {} | |
# collect fields | |
for struct_id, struct_type_name_pairs in blend_sdna_structs: | |
struct_name = blend_sdna_types[struct_id] | |
ctype_struct = ctypes_structs[struct_name] | |
fields = [] | |
for stype, sname in struct_type_name_pairs: | |
name_string = blend_sdna_names[sname] | |
type_string = blend_sdna_types[stype] | |
type_py = ctypes_basic.get(type_string) | |
if type_py is None: | |
type_py = ctypes_structs.get(type_string) | |
# todo, these might need to be changed | |
name_string = name_string.replace(b"(", b"") | |
name_string = name_string.replace(b")", b"") | |
# * First parse the pointer * | |
pointer_count = 0 | |
while name_string[0] == 42: # '*' | |
pointer_count += 1 | |
name_string = name_string[1:] | |
# alredy a pointer | |
if type_py is ctypes.c_void_p: | |
pointer_count -= 1 | |
elif type_py is ctypes.c_char and pointer_count == 1: | |
type_py = ctypes.c_char_p | |
pointer_count = 0 | |
if pointer_count < 0: | |
Exception("error parsing pointer") | |
for i in range(pointer_count): | |
type_py = ctypes.POINTER(type_py) | |
# * Now parse the array [] * | |
if b'[' in name_string: | |
name_string = name_string.replace(b'[', b' ') | |
name_string = name_string.replace(b']', b' ') | |
name_split = name_string.split() | |
name_string = name_split[0] | |
for array_dim in reversed(name_split[1:]): | |
type_py = type_py * int(array_dim) | |
fields.append((name_string.decode(), type_py)) | |
ctypes_fields[struct_name] = fields | |
# apply fields all in one go! | |
for struct_id, struct_type_name_pairs in blend_sdna_structs: | |
struct_name = blend_sdna_types[struct_id] | |
ctype_struct = ctypes_structs[struct_name] | |
try: | |
ctype_struct._fields_ = ctypes_fields[struct_name] | |
except: | |
''' | |
print("Error:", struct_name) | |
import traceback | |
traceback.print_exc() | |
''' | |
pass | |
# print(fields) | |
# test fields | |
#for struct_id, struct_type_name_pairs in blend_sdna_structs: | |
# ctype_struct = ctypes_structs[blend_sdna_types[struct_id]] | |
# if blend_sdna_typelens[struct_id] != ctypes.sizeof(ctype_struct): | |
# print("Size Mismatch for %r blender:%d vs python:%d" % (blend_sdna_types[struct_id], blend_sdna_typelens[struct_id], ctypes.sizeof(ctype_struct))) | |
return ctypes_structs | |
def decorate_api(struct_dict): | |
# * Decotate the api * | |
# listbase iter | |
type_cast_lb = struct_dict[b'ListBase'] | |
type_cast_link = struct_dict[b'Link'] | |
def list_base_iter(self, type_name): | |
if type(type_name) == str: | |
type_cast = struct_dict[type_name.encode('ASCII')] | |
else: | |
# allow passing types direcly | |
type_cast = type_name | |
# empty listbase | |
if self.first is None: | |
ret = None | |
else: | |
try: | |
ret = type_cast_link.from_address(ctypes.addressof(self.first)) | |
except TypeError: | |
ret = type_cast_link.from_address(self.first) | |
while ret is not None: | |
return_value = type_cast.from_address(ctypes.addressof(ret)) | |
try: | |
next_pointer = getattr(ret.next, "contents") | |
except: | |
next_pointer = None | |
if next_pointer: | |
ret = type_cast_link.from_address(ctypes.addressof(next_pointer)) | |
else: | |
ret = None | |
yield return_value | |
struct_dict[b'ListBase'].ITER = list_base_iter | |
def CAST(self, to): | |
if type(type_name) == str: | |
type_cast = struct_dict[to.encode('ASCII')] | |
else: | |
type_cast = to | |
return type_cast.from_address(ctypes.addressof(self)) | |
MixIn.CAST = CAST | |
blend_sdna_names, blend_sdna_types, blend_sdna_typelens, blend_sdna_structs = blend_parse_dna() | |
struct_dict = create_dna_structs(blend_sdna_names, blend_sdna_types, blend_sdna_typelens, blend_sdna_structs) | |
def printStructs(showStructs=None): | |
for struct_id, struct_type_name_pairs in blend_sdna_structs: | |
struct_name = blend_sdna_types[struct_id].decode() | |
if not showStructs or struct_name in showStructs: | |
print("typedef struct %s {" % struct_name) | |
for stype, sname in struct_type_name_pairs: | |
print(" %s %s;" % (blend_sdna_types[stype].decode(), blend_sdna_names[sname].decode())) | |
print("} %s;" % struct_name) | |
decorate_api(struct_dict) # not essential but useful | |
# manually wrap Main | |
Main = type("Main", (ctypes.Structure, ), {}) | |
_lb = struct_dict[b"ListBase"] | |
Main._fields_ = [("next", ctypes.POINTER(Main)), | |
("prev", ctypes.POINTER(Main)), | |
("name", ctypes.c_char * 240), | |
("versionfile", ctypes.c_short), | |
("subversionfile", ctypes.c_short), | |
("minversionfile", ctypes.c_short), | |
("minsubversionfile", ctypes.c_short), | |
("revision", ctypes.c_int), | |
("curlib", ctypes.POINTER(struct_dict[b"Library"])), | |
("scene", _lb), | |
("library", _lb), | |
("object", _lb), | |
("mesh", _lb), | |
("curve", _lb), | |
("mball", _lb), | |
("mat", _lb), | |
("tex", _lb), | |
("image", _lb), | |
("latt", _lb), | |
("lamp", _lb), | |
("camera", _lb), | |
("ipo", _lb), | |
("key", _lb), | |
("world", _lb), | |
("screen", _lb), | |
("script", _lb), | |
("vfont", _lb), | |
("text", _lb), | |
("sound", _lb), | |
("group", _lb), | |
("armature", _lb), | |
("action", _lb), | |
("nodetree", _lb), | |
("brush", _lb), | |
("particle", _lb), | |
("wm", _lb), | |
("gpencil", _lb), | |
("movieclip", _lb), | |
("mask",_lb), | |
("linestyle",_lb) | |
] | |
del _lb | |
# import bpy | |
# main = Main.from_address(bpy.data.as_pointer()) | |
# main is the first pointer in Global. | |
main_address = ctypes.POINTER(ctypes.c_void_p).from_address(ctypes.addressof(blend_cdll.G)).contents.value | |
main = Main.from_address(main_address) | |
return main, struct_dict, printStructs | |
main, _struct_dict, printStructs = _api() | |
# types dict | |
types = type(ctypes)("pydna.types") | |
types.__dict__.update({s.__name__: s for s in _struct_dict.values()}) | |
# --- Image extension component monkey-class into RNA, pydna.py above from r34522. | |
def setup(): | |
# only wrap enough to read image buffer | |
ImBuf = type("ImBuf", (ctypes.Structure, ), {}) | |
ImBuf._fields_ = [("next", ctypes.POINTER(ImBuf)), | |
("prev", ctypes.POINTER(ImBuf)), | |
("x", ctypes.c_int), | |
("y", ctypes.c_int), | |
("depth", ctypes.c_ubyte), | |
("channels", ctypes.c_int), | |
("flags", ctypes.c_int), | |
("mall", ctypes.c_int), | |
("rect", ctypes.POINTER(ctypes.c_ubyte)), | |
("crect", ctypes.POINTER(ctypes.c_ubyte)), | |
("rect_float", ctypes.POINTER(ctypes.c_float)), | |
] | |
# pretend we're external | |
pydna = type(ctypes)("pydna") | |
pydna.__dict__.update(globals()) | |
def _image_get_buffer(image): | |
image_dna = pydna.types.Image.from_address(image.as_pointer()) | |
for ibuf in image_dna.ibufs.ITER(ImBuf): | |
return ibuf.x, ibuf.y, ibuf.rect, ibuf.rect_float | |
return 0, 0, (), () | |
def _get_dna(obj): | |
_dna = pydna.types.Object.from_address(obj.as_pointer()) | |
return _dna | |
import bpy | |
bpy.types.Image.buffer_raw = property(_image_get_buffer) | |
bpy.types.Action._dna = property(_get_dna) | |
bpy.types.Armature._dna = property(_get_dna) | |
bpy.types.Brush._dna = property(_get_dna) | |
bpy.types.Camera._dna = property(_get_dna) | |
bpy.types.Curve._dna = property(_get_dna) | |
#bpy.types.Group._dna = property(_get_dna) | |
bpy.types.Image._dna = property(_get_dna) | |
#bpy.types.Lamp._dna = property(_get_dna) | |
bpy.types.Lattice._dna = property(_get_dna) | |
bpy.types.Library._dna = property(_get_dna) | |
bpy.types.Mask._dna = property(_get_dna) | |
bpy.types.MovieClip._dna = property(_get_dna) | |
bpy.types.NodeTree._dna = property(_get_dna) | |
bpy.types.Palette._dna = property(_get_dna) | |
bpy.types.Particle._dna = property(_get_dna) | |
bpy.types.Screen._dna = property(_get_dna) | |
bpy.types.Speaker._dna = property(_get_dna) | |
bpy.types.World._dna = property(_get_dna) | |
bpy.types.Object._dna = property(_get_dna) | |
bpy.types.NodeTree._dna = property(_get_dna) | |
bpy.types.Scene._dna = property(_get_dna) | |
bpy.types.Sound._dna = property(_get_dna) | |
bpy.types.Texture._dna = property(_get_dna) | |
bpy.types.Text._dna = property(_get_dna) | |
bpy.types.Material._dna = property(_get_dna) | |
bpy.types.Mesh._dna = property(_get_dna) | |
Can confirm that have the same error as above (Blender 3.6).
Error: Python: Traceback (most recent call last):
File "\Text", line 342, in <module>
File "\Text", line 76, in _api
File "\Blender\3.6\python\lib\ctypes\__init__.py", line 374, in __init__
self._handle = _dlopen(self._name, mode)
OSError: [WinError 87] The parameter is incorrect
Can confirm that have the same error as above (Blender 3.6).
Error: Python: Traceback (most recent call last): File "\Text", line 342, in <module> File "\Text", line 76, in _api File "\Blender\3.6\python\lib\ctypes\__init__.py", line 374, in __init__ self._handle = _dlopen(self._name, mode) OSError: [WinError 87] The parameter is incorrect
Yeah, seems like that this don't work anymore as the internal structure seems to be changed. Those were very fragile calls from the get go as we call manually c-calls. Didn't do lots of blender-scripting lately but maybe there is an official way for this already? Maybe @ideasman42 knows more as he wrote the original script....
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This gist is quite old and the structure of blender changed quite a bit. I tested it as a script in the Editor and only had to remove bpy.types.Group and Lamp and it still worked. Was actually quite surprised it did :D