Skip to content

Commit f1f53ef

Browse files
committed
feat: Add AOT support for returning Class types and enhance method call generation with object type handling
1 parent 592a07d commit f1f53ef

4 files changed

Lines changed: 177 additions & 33 deletions

File tree

NativeScript/runtime/NativeScriptAOT.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ void __ns_aot_return_bool(NSAOTCallInfo info, BOOL value);
3838
void __ns_aot_return_double(NSAOTCallInfo info, double value);
3939
void __ns_aot_return_struct(NSAOTCallInfo info, const void* data,
4040
const char* structName);
41+
void __ns_aot_return_class(NSAOTCallInfo info, Class value);
4142

4243
// --- Exception handling ---
4344
void __ns_aot_throw_exception(NSAOTCallInfo info, id exception);

NativeScript/runtime/NativeScriptAOTBridge.mm

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,27 @@ void __ns_aot_return_struct(NSAOTCallInfo _info, const void* data, const char* s
261261
info.GetReturnValue().Set(result);
262262
}
263263

264+
void __ns_aot_return_class(NSAOTCallInfo _info, Class value) {
265+
auto& info = *reinterpret_cast<const FunctionCallbackInfo<Value>*>(_info);
266+
Isolate* isolate = info.GetIsolate();
267+
if (value == nil) {
268+
info.GetReturnValue().Set(Null(isolate));
269+
return;
270+
}
271+
auto cache = tns::Caches::Get(isolate);
272+
Class klass = value;
273+
while (klass != nil) {
274+
std::string name = class_getName(klass);
275+
auto it = cache->CtorFuncs.find(name);
276+
if (it != cache->CtorFuncs.end()) {
277+
info.GetReturnValue().Set(it->second->Get(isolate));
278+
return;
279+
}
280+
klass = class_getSuperclass(klass);
281+
}
282+
info.GetReturnValue().Set(Null(isolate));
283+
}
284+
264285
void __ns_aot_throw_exception(NSAOTCallInfo _info, id exception) {
265286
auto& info = *reinterpret_cast<const FunctionCallbackInfo<Value>*>(_info);
266287
Isolate* isolate = info.GetIsolate();

scripts/generate-aot.py

Lines changed: 41 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -175,9 +175,9 @@ def method_stub_name(cls, sel):
175175
return f"AOT_{cls}_{sanitize_selector(sel)}"
176176

177177

178-
def build_objc_call(cls, sel, args, target="target", is_static=False, class_var=None, ret=None):
178+
def build_objc_call(cls, sel, args, target="target", is_static=False, class_var=None, ret=None, object_types=frozenset()):
179179
if class_var:
180-
return _build_msgsend_call(class_var if is_static else target, sel, ret or "id", args)
180+
return _build_msgsend_call(class_var if is_static else target, sel, ret or "id", args, object_types)
181181
if is_static:
182182
receiver = cls
183183
else:
@@ -189,23 +189,27 @@ def build_objc_call(cls, sel, args, target="target", is_static=False, class_var=
189189
return f"[{receiver} {expr}]"
190190

191191

192-
def _c_type(t):
193-
return t if t not in TYPES else TYPES[t]["c_type"]
192+
def _c_type(t, object_types=frozenset()):
193+
if t in TYPES:
194+
return TYPES[t]["c_type"]
195+
if t in object_types:
196+
return "id"
197+
return t
194198

195199

196-
def _build_msgsend_call(receiver, sel, ret, args):
197-
c_ret = _c_type(ret)
198-
c_args = ["id", "SEL"] + [_c_type(a) for a in args]
200+
def _build_msgsend_call(receiver, sel, ret, args, object_types=frozenset()):
201+
c_ret = _c_type(ret, object_types)
202+
c_args = ["id", "SEL"] + [_c_type(a, object_types) for a in args]
199203
cast = f"(({c_ret}(*)({', '.join(c_args)}))objc_msgSend)"
200204
arg_str = ", ".join(f"arg{i}" for i in range(len(args)))
201205
suffix = f", {arg_str}" if arg_str else ""
202206
return f"{cast}((id){receiver}, @selector({sel}){suffix})"
203207

204208

205-
def build_super_call(cls, sel, ret, args, struct_tag=False):
209+
def build_super_call(cls, sel, ret, args, struct_tag=False, object_types=frozenset()):
206210
prefix = "struct " if struct_tag else ""
207-
c_ret = _c_type(ret)
208-
c_args = [f"{prefix}objc_super*", "SEL"] + [_c_type(a) for a in args]
211+
c_ret = _c_type(ret, object_types)
212+
c_args = [f"{prefix}objc_super*", "SEL"] + [_c_type(a, object_types) for a in args]
209213
cast = f"(({c_ret}(*)({', '.join(c_args)}))"
210214
arg_str = ", ".join(f"arg{i}" for i in range(len(args)))
211215
suffix = f", {arg_str}" if arg_str else ""
@@ -715,28 +719,32 @@ def generate(config_path, output_dir):
715719
"NSString": "__ns_aot_return_string(info, {result})",
716720
"instancetype": "__ns_aot_return_object(info, {result})",
717721
"BOOL": "__ns_aot_return_bool(info, {result})",
718-
"Class": "__ns_aot_return_id(info, (id){result})",
722+
"Class": "__ns_aot_return_class(info, {result})",
719723
}
720724

721725

722-
def external_arg_expr(arg_type, i):
726+
def external_arg_expr(arg_type, i, object_types=frozenset()):
723727
if arg_type in EXTERNAL_ARG:
724728
return EXTERNAL_ARG[arg_type].format(i=i)
729+
if arg_type in object_types:
730+
return EXTERNAL_ARG["id"].format(i=i)
725731
if arg_type in TYPES:
726732
return f"({TYPES[arg_type]['c_type']})__ns_aot_arg_double(info, {i})"
727733
return None
728734

729735

730-
def is_struct_type(ret_type):
731-
return ret_type not in TYPES and ret_type not in EXTERNAL_RET
736+
def is_struct_type(ret_type, object_types=frozenset()):
737+
return ret_type not in TYPES and ret_type not in EXTERNAL_RET and ret_type not in object_types
732738

733739

734-
def external_ret_call(ret_type, result_var):
740+
def external_ret_call(ret_type, result_var, object_types=frozenset()):
735741
if ret_type == "void":
736742
return None
737743
if ret_type in EXTERNAL_RET:
738744
return EXTERNAL_RET[ret_type].format(result=result_var)
739-
if is_struct_type(ret_type):
745+
if ret_type in object_types:
746+
return EXTERNAL_RET["id"].format(result=result_var)
747+
if is_struct_type(ret_type, object_types):
740748
return f'__ns_aot_return_struct(info, &{result_var}, "{ret_type}")'
741749
return f"__ns_aot_return_double(info, (double){result_var})"
742750

@@ -745,18 +753,18 @@ def class_var_name(cls):
745753
return f"_cls_{cls}"
746754

747755

748-
def gen_external_method_stub(method, swift_classes):
756+
def gen_external_method_stub(method, object_types=frozenset(), protocol_types=frozenset(), swift_classes=frozenset()):
749757
cls = method["class"]
750758
sel = method["selector"]
751759
ret = method["ret"]
752760
args = method["args"]
753761
is_static = method.get("static", False)
754762
name = method_stub_name(cls, sel)
755763
is_void = ret == "void"
756-
is_struct = is_struct_type(ret)
757-
c_ret_type = ret if is_struct else TYPES[ret]["c_type"]
758-
is_swift = cls in swift_classes
759-
cvar = class_var_name(cls) if is_swift else None
764+
is_obj = ret in object_types
765+
is_struct = is_struct_type(ret, object_types)
766+
c_ret_type = "id" if is_obj else (ret if is_struct else TYPES[ret]["c_type"])
767+
needs_msgsend = cls in protocol_types or cls in swift_classes
760768

761769
sign = "+" if is_static else "-"
762770
lines = []
@@ -772,9 +780,9 @@ def gen_external_method_stub(method, swift_classes):
772780
lines.append(" if (target == nil) return;")
773781

774782
for i, arg in enumerate(args):
775-
expr = external_arg_expr(arg, i)
783+
expr = external_arg_expr(arg, i, object_types)
776784
if expr is not None:
777-
c_type = TYPES[arg]['c_type'] if arg in TYPES else arg
785+
c_type = "id" if arg in object_types else (TYPES[arg]['c_type'] if arg in TYPES else arg)
778786
lines.append(f" {c_type} arg{i} = {expr};")
779787
else:
780788
lines.append(f" {arg} arg{i};")
@@ -785,14 +793,17 @@ def gen_external_method_stub(method, swift_classes):
785793
lines.append(f" {c_ret_type} result;")
786794

787795
if is_static:
788-
static_call = _build_msgsend_call("_cls", sel, ret, args)
796+
static_call = _build_msgsend_call("_cls", sel, ret, args, object_types)
789797
if is_void:
790798
lines.append(f" {static_call};")
791799
else:
792800
lines.append(f" result = {static_call};")
793801
else:
794-
objc_call = build_objc_call(cls, sel, args, is_static=False, class_var=cvar, ret=ret)
795-
super_call = build_super_call(cls, sel, ret, args, struct_tag=True)
802+
if needs_msgsend:
803+
objc_call = _build_msgsend_call("target", sel, ret, args, object_types)
804+
else:
805+
objc_call = build_objc_call(cls, sel, args, is_static=False, ret=ret, object_types=object_types)
806+
super_call = build_super_call(cls, sel, ret, args, struct_tag=True, object_types=object_types)
796807
lines.append(" if (callSuper) {")
797808
lines.append(" struct objc_super sup = {target, class_getSuperclass(object_getClass(target))};")
798809
if is_void:
@@ -806,7 +817,7 @@ def gen_external_method_stub(method, swift_classes):
806817
lines.append(f" result = {objc_call};")
807818
lines.append(" }")
808819

809-
ret_call = external_ret_call(ret, "result")
820+
ret_call = external_ret_call(ret, "result", object_types)
810821
if ret_call:
811822
lines.append(f" {ret_call};")
812823

@@ -878,6 +889,8 @@ def generate_external(config_path, output_path):
878889
methods = _dedup_methods(config.get("methods", []))
879890
imports = config.get("imports", [])
880891
swift_classes = set(config.get("swiftClasses", []))
892+
object_types = frozenset(config.get("objectTypes", []))
893+
protocol_types = frozenset(config.get("protocolTypes", []))
881894

882895
extra_imports = [i if i.startswith("#") else f"#import <{i}/{i}.h>" for i in imports]
883896
if extra_imports:
@@ -890,7 +903,7 @@ def generate_external(config_path, output_path):
890903
parts.append("")
891904

892905
for m in methods:
893-
parts.append(gen_external_method_stub(m, swift_classes))
906+
parts.append(gen_external_method_stub(m, object_types, protocol_types, swift_classes))
894907
parts.append("")
895908

896909
parts.append(gen_external_registration(methods, swift_classes))

scripts/resolve-aot-imports.py

Lines changed: 114 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -210,25 +210,74 @@ def parse_json_file(json_path):
210210
return module
211211

212212

213+
def _collect_protocol_names_json(json_path):
214+
"""Extract Protocol names from a metadata JSON file."""
215+
names = set()
216+
with open(json_path) as f:
217+
data = json.load(f)
218+
for item in data.get("Items", []):
219+
if item.get("Type") == "Protocol":
220+
names.add(item.get("Name", ""))
221+
js = item.get("JsName", "")
222+
if js:
223+
names.add(js)
224+
names.discard("")
225+
return names
226+
227+
228+
def _collect_protocol_names_yaml(yaml_path):
229+
"""Extract Protocol names from a metadata YAML file."""
230+
names = set()
231+
current_name = None
232+
current_js_name = None
233+
with open(yaml_path, errors="replace") as f:
234+
for line in f:
235+
m = re.match(r"^ - Name:\s+(.+)$", line)
236+
if m:
237+
current_name = m.group(1).strip().strip("'\"")
238+
current_js_name = None
239+
continue
240+
if current_name is not None:
241+
m = re.match(r"^ JsName:\s+(.+)$", line)
242+
if m:
243+
current_js_name = m.group(1).strip().strip("'\"")
244+
continue
245+
m = re.match(r"^ Type:\s+(\S+)", line)
246+
if m:
247+
if m.group(1) == "Protocol":
248+
names.add(current_name)
249+
if current_js_name:
250+
names.add(current_js_name)
251+
current_name = None
252+
current_js_name = None
253+
continue
254+
return names
255+
256+
213257
def scan_metadata_dir(metadata_dir):
214258
"""Build lookup maps from all JSON or YAML files.
215259
216260
Prefers JSON files when present; falls back to YAML.
217261
218262
Returns:
263+
(name_to_entries, protocol_names)
219264
name_to_entries: dict mapping Name or JsName → [(ModuleInfo, InterfaceInfo), ...]
265+
protocol_names: set of protocol type names
220266
"""
221267
name_to_entries = {}
268+
protocol_names = set()
222269

223270
json_files = sorted(glob.glob(os.path.join(metadata_dir, "*.json")))
224271
yaml_files = sorted(glob.glob(os.path.join(metadata_dir, "*.yaml")))
225272

226273
if json_files:
227274
files_and_parser = [(p, parse_json_file) for p in json_files]
275+
proto_collector = _collect_protocol_names_json
228276
elif yaml_files:
229277
files_and_parser = [(p, parse_yaml) for p in yaml_files]
278+
proto_collector = _collect_protocol_names_yaml
230279
else:
231-
return name_to_entries
280+
return name_to_entries, protocol_names
232281

233282
for path, parser in files_and_parser:
234283
if os.path.basename(path).startswith("metadata-generation"):
@@ -241,8 +290,9 @@ def scan_metadata_dir(metadata_dir):
241290
name_to_entries.setdefault(iface.name, []).append(entry)
242291
if iface.js_name != iface.name:
243292
name_to_entries.setdefault(iface.js_name, []).append(entry)
293+
protocol_names.update(proto_collector(path))
244294

245-
return name_to_entries
295+
return name_to_entries, protocol_names
246296

247297

248298
def pick_module(cls, entries):
@@ -297,6 +347,22 @@ def import_for_swift(iface):
297347
return None
298348

299349

350+
def import_for_framework(mod, iface):
351+
"""Derive the framework import for an interface.
352+
353+
For non-system frameworks, uses the specific header from the Filename field
354+
(e.g. #import <TNSListView/TKCollectionView.h>) since they may not have an
355+
umbrella header. System frameworks always use the umbrella header.
356+
"""
357+
if mod.is_system:
358+
return f"#import <{mod.name}/{mod.name}.h>"
359+
if iface.filename:
360+
m = re.search(r'\.framework/Headers/(.+\.h)$', iface.filename)
361+
if m:
362+
return f"#import <{mod.name}/{m.group(1)}>"
363+
return f"#import <{mod.name}/{mod.name}.h>"
364+
365+
300366
def resolve(config_path, yaml_dir):
301367
with open(config_path) as f:
302368
config = json.load(f)
@@ -320,7 +386,7 @@ def resolve(config_path, yaml_dir):
320386
return
321387

322388
print(f"Scanning {yaml_dir} for {len(class_names)} classes...")
323-
name_to_entries = scan_metadata_dir(yaml_dir)
389+
name_to_entries, protocol_names = scan_metadata_dir(yaml_dir)
324390

325391
needed_imports = set()
326392
unresolved = []
@@ -344,8 +410,9 @@ def resolve(config_path, yaml_dir):
344410
elif mod.is_system and not mod.is_framework:
345411
print(f" {cls}{mod.name} (non-framework system, covered by Foundation)")
346412
elif mod.is_framework:
347-
needed_imports.add(f"#import <{mod.name}/{mod.name}.h>")
348-
print(f" {cls}{mod.name}")
413+
imp = import_for_framework(mod, iface)
414+
needed_imports.add(imp)
415+
print(f" {cls}{mod.name} ({imp})")
349416
else:
350417
needed_imports.add(f"#import <{mod.name}.h>")
351418
print(f" {cls}{mod.name} (non-framework)")
@@ -355,6 +422,30 @@ def resolve(config_path, yaml_dir):
355422
for cls in unresolved:
356423
print(f" - {cls}")
357424

425+
# Detect ObjC object types used in args/ret that aren't primitives.
426+
# Types found as Interface in metadata are objects (passed as id),
427+
# everything else unknown is treated as a struct by the generator.
428+
PRIMITIVE_TYPES = {
429+
"void", "BOOL", "id", "instancetype", "NSString", "SEL", "Class",
430+
"int", "uint", "long", "ulong", "longlong", "ulonglong",
431+
"float", "double", "char", "uchar", "short", "ushort",
432+
}
433+
all_types = set()
434+
for m in methods:
435+
all_types.add(m["ret"])
436+
all_types.update(m["args"])
437+
unknown_types = all_types - PRIMITIVE_TYPES
438+
object_types = set()
439+
for t in sorted(unknown_types):
440+
if t in name_to_entries:
441+
object_types.add(t)
442+
if object_types:
443+
print(f"\n Object types (not structs): {', '.join(sorted(object_types))}")
444+
445+
used_protocols = set(c for c in class_names if c in protocol_names)
446+
if used_protocols:
447+
print(f"\n Protocol types (use objc_msgSend): {', '.join(sorted(used_protocols))}")
448+
358449
# Detect static methods
359450
changed = False
360451
static_changes = []
@@ -393,6 +484,24 @@ def resolve(config_path, yaml_dir):
393484
del config["swiftClasses"]
394485
changed = True
395486

487+
obj_list = sorted(object_types)
488+
existing_obj = sorted(config.get("objectTypes", []))
489+
if obj_list != existing_obj:
490+
if obj_list:
491+
config["objectTypes"] = obj_list
492+
elif "objectTypes" in config:
493+
del config["objectTypes"]
494+
changed = True
495+
496+
proto_list = sorted(used_protocols)
497+
existing_proto = sorted(config.get("protocolTypes", []))
498+
if proto_list != existing_proto:
499+
if proto_list:
500+
config["protocolTypes"] = proto_list
501+
elif "protocolTypes" in config:
502+
del config["protocolTypes"]
503+
changed = True
504+
396505
if not changed:
397506
print(f"\n Config already up to date.")
398507
return

0 commit comments

Comments
 (0)