unity-dotnet P4: Muck runs its menu scene's scripts on .NET Mono (batch mode)

- mono_class_get_field_from_name retries Unity mscorlib's m_x as .NET's _x
  (AttributeScannerQuery reads AttributeUsageAttribute's fields by name);
  misses are logged.
- mono_custom_attrs_from_field uses the field's own class: UnityPlayer's
  serializer passes the class it serializes (List`1 for an element's fields).
- Interp-to-native wrappers for every function type in the provider: the
  provider is linked once, its type section read, m2n.c regenerated, linked
  again (fixes CANNOT HANDLE INTERP ICALL SIG LLLLLLLL).
- build.sh: JOBS (default 12, nice 19) per the gooner rules.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01PeRutwFgZ66XpxjasKrPn1
This commit is contained in:
velzieandClaude Opus 5.5 committed 2026-10-04 23:27:42 -04:00
1 parent 1fda117438
commit 21dbdeab3c
3 files changed
+110 -16

No files matched your search

+21 -12
View File
@@ -20,7 +20,7 @@
# em-runtime em-native em-smoke P1's emscripten build and its node smoke test
#
# Needs clang/wasm-ld (22), llvm-ar, dotnet, git, ninja, cmake; /usr/lib/emsdk only for em-*.
# Builds at nice 19 with at most 4 jobs (stream rules).
# Builds at nice 19 with JOBS (default 12) jobs (stream rules).
set -euo pipefail
FORK=https://github.com/r58Playz/dotnet-runtime.git
@@ -33,6 +33,7 @@ OUT=$(realpath -m "$1"); shift
STEPS=${*:-src sysroot runtime native corelib provider}
MUSL_SRC=${MUSL_SRC:-$(realpath -m "$UD/../../../musl-wasm64")}
RUN="nice -n 19"
JOBS=${JOBS:-12} # gooner rules: a heavy nice-19 build may use 12 jobs
# CMake for dotnet/runtime wants llvm-strings, llvm-readelf, llvm-link: from the versioned LLVM if not on PATH.
command -v llvm-strings >/dev/null || PATH=$PATH:/usr/lib/llvm-22/bin
SR=$OUT/sysroot/sysroot
@@ -63,7 +64,7 @@ step_runtime() {
-DFEATURE_PERFTRACING_DISABLE_PERFTRACING_LISTEN_PORTS=1 -DFEATURE_PERFTRACING_DISABLE_DEFAULT_LISTEN_PORT=1 \
-DFEATURE_PERFTRACING_DISABLE_CONNECT_PORTS=1 -DWITH_OPTIM=OFF \
"$OUT/src/src/mono" > "$OUT/runtime-configure.log"
$RUN ninja -j4 > "$OUT/runtime-build.log"
$RUN ninja -j"$JOBS" > "$OUT/runtime-build.log"
}
step_native() {
@@ -73,7 +74,7 @@ step_native() {
-DCLR_CMAKE_TARGET_ARCH=wasm -DCLR_CMAKE_TARGET_OS=linux -DWITH_OPTIM=OFF -DGEX_NO_GLOBALIZATION_NATIVE=1 \
> "$OUT/native-configure.log"
cd "$OUT/objnative"
$RUN ninja -j4 System.Native/libSystem.Native.a > "$OUT/native-build.log"
$RUN ninja -j"$JOBS" System.Native/libSystem.Native.a > "$OUT/native-build.log"
}
step_corelib() {
@@ -100,13 +101,21 @@ step_provider() {
done
clang $WASM64_CFLAGS $INC -I"$N/libs/Common" -I"$OUT/objnative/Common" -I"$N/libs/System.Native" -I"$N" -I"$N/libs" \
-c "$N/libs/System.Native/entrypoints.c" -o entrypoints-System.Native.o
wasm-ld -mwasm64 --experimental-pic -pie --no-entry --shared-memory --import-memory --max-memory=17179869184 -o mono.wasm \
gexmono-provider.o gexmono-unity.o gexprov64.o gexprov64-table.o adapters.o m2n.o entrypoints-System.Native.o "$OUT/objnative/System.Native/libSystem.Native.a" \
"$L"/mini/libmonosgen-2.0.a "$L"/mini/libmono-ee-interp.a "$L"/mini/libmono-component-marshal-ilgen-static.a \
"$L"/mini/libmono-component-hot_reload-stub-static.a "$L"/mini/libmono-component-debugger-stub-static.a \
"$L"/mini/libmono-component-diagnostics_tracing-stub-static.a "$L"/mini/libmono-wasm-nosimd.a \
"$L"/minipal/minipal/libminipal.a "$OUT/obj/_deps/fetchzlibng-build/libz.a" \
-L"$SR/lib" -lc "$SR/lib/libclang_rt.builtins-wasm64.a"
link() {
wasm-ld -mwasm64 --experimental-pic -pie --no-entry --shared-memory --import-memory --max-memory=17179869184 -o mono.wasm \
gexmono-provider.o gexmono-unity.o gexprov64.o gexprov64-table.o adapters.o m2n.o entrypoints-System.Native.o "$OUT/objnative/System.Native/libSystem.Native.a" \
"$L"/mini/libmonosgen-2.0.a "$L"/mini/libmono-ee-interp.a "$L"/mini/libmono-component-marshal-ilgen-static.a \
"$L"/mini/libmono-component-hot_reload-stub-static.a "$L"/mini/libmono-component-debugger-stub-static.a \
"$L"/mini/libmono-component-diagnostics_tracing-stub-static.a "$L"/mini/libmono-wasm-nosimd.a \
"$L"/minipal/minipal/libminipal.a "$OUT/obj/_deps/fetchzlibng-build/libz.a" \
-L"$SR/lib" -lc "$SR/lib/libclang_rt.builtins-wasm64.a"
}
# Interp-to-native wrappers for every function type in the provider (gen-m2n.py): link, read
# the types, regenerate them, link again.
link
python3 "$HERE/host/gen-m2n.py" "$HERE/host/m2n-extra.txt" --types-from mono.wasm > m2n.c
clang $WASM64_CFLAGS $INC -Wno-unused-command-line-argument -c m2n.c -o m2n.o
link
echo "provider: $W/mono.wasm; $(tail -1 adapters.log | cut -d, -f1)"
}
@@ -125,7 +134,7 @@ step_em-runtime() {
-DFEATURE_PERFTRACING_DISABLE_DEFAULT_LISTEN_PORT=1 \
"-DCMAKE_ASM_FLAGS=$F" "-DCMAKE_C_FLAGS=$F -D_GNU_SOURCE=1 -mnontrapping-fptoint" \
"-DCMAKE_CXX_FLAGS=$F -D_GNU_SOURCE=1 -mnontrapping-fptoint" "$OUT/src/src/mono" > "$OUT/em-runtime-configure.log"
$RUN ninja -j4 > "$OUT/em-runtime-build.log"
$RUN ninja -j"$JOBS" > "$OUT/em-runtime-build.log"
}
step_em-native() {
@@ -136,7 +145,7 @@ step_em-native() {
-DCLR_CMAKE_TARGET_OS=browser -DCLR_CMAKE_TARGET_ARCH=wasm -DCLR_CMAKE_HOST_ARCH=wasm -DMONO_WASM_MT=1 \
"-DCMAKE_C_FLAGS=$F" "-DCMAKE_CXX_FLAGS=$F" > "$OUT/em-native-configure.log"
cd "$OUT/objnative-em"
$RUN ninja -j4 System.Native/libSystem.Native.a > "$OUT/em-native-build.log"
$RUN ninja -j"$JOBS" System.Native/libSystem.Native.a > "$OUT/em-native-build.log"
local N="$OUT/src/src/native"
emcc $F -O2 -c "$N/libs/System.Native/entrypoints.c" -I"$N/libs/Common" -I"$OUT/objnative-em/Common" \
-I"$N/libs/System.Native" -I"$N" -I"$N/libs" -o "$OUT/objnative-em/entrypoints-System.Native.o"
+40 -3
View File
@@ -3,8 +3,12 @@
A cookie names the callee's wasm signature: the return, then each argument, with
I = i32, L = i64 (pointer-sized values included on wasm64), F = f32, D = f64, V = void.
Every cookie up to MAX_FULL arguments is generated; longer ones come from the extra list.
On a 64-bit runtime each integer argument (I or L) takes one slot of margs->iargs."""
Every cookie up to MAX_FULL arguments is generated; longer ones come from the extra list and,
with --types-from WASM, from every function type of a first link of the provider (a native
callee in the provider has one of them), so build.sh links twice.
On a 64-bit runtime each integer argument (I or L) takes one slot of margs->iargs.
gen-m2n.py [EXTRA] [--types-from WASM] > m2n.c"""
import itertools, sys
MAX_FULL = 4
@@ -37,8 +41,41 @@ def wrapper(c):
body = f"\t{call};" if ret == 'V' else f"\t*({CT[ret]} *)mono_wasm_interp_method_args_get_retval (margs) = {call};"
return f"static void m2n_{c} (void *fn, InterpMethodArguments *margs)\n{{\n{body}\n}}\n"
def leb(d, i):
r = sh = 0
while True:
b = d[i]; i += 1
r |= (b & 0x7f) << sh; sh += 7
if b < 0x80:
return r, i
def wasm_cookies(path):
"""Cookies for the function types in a wasm module's type section."""
d = open(path, 'rb').read()
VT = {0x7f: 'I', 0x7e: 'L', 0x7d: 'F', 0x7c: 'D'}
i, out = 8, set()
while i < len(d):
sid = d[i]; size, i = leb(d, i + 1)
if sid == 1:
n, j = leb(d, i)
for _ in range(n):
if d[j] != 0x60:
raise SystemExit(f"{path}: unexpected type form {d[j]:#x}")
np_, j = leb(d, j + 1); ps = d[j:j + np_]; j += np_
nr, j = leb(d, j); rs = d[j:j + nr]; j += nr
if nr <= 1 and all(b in VT for b in ps + rs):
out.add((VT[rs[0]] if nr else 'V') + ''.join(VT[b] for b in ps))
return out
i += size
return out
def main():
extra = [l.strip() for l in open(sys.argv[1]) if l.strip() and not l.startswith('#')] if len(sys.argv) > 1 else []
args = sys.argv[1:]
types = set()
if '--types-from' in args:
k = args.index('--types-from'); types = wasm_cookies(args[k + 1]); del args[k:k + 2]
extra = [l.strip() for l in open(args[0]) if l.strip() and not l.startswith('#')] if args else []
extra += [c for c in types if len(c) <= 24]
cs = cookies(extra)
print("/* GENERATED by gen-m2n.py: Mono interp-to-native wrappers for wasm64 (gex). */")
print("#include <stdint.h>\n#include <string.h>\n#include <stdlib.h>")
+49 -1
View File
@@ -102,6 +102,54 @@ U(mono_unity_type_get_name_full_chunked)
RETV;
}
/* Unity's mscorlib is .NET Framework's reference source, whose private
* fields are m_name; .NET's CoreLib names most of the same fields _name.
* UnityPlayer reads a few of them by name (AttributeUsageAttribute's
* m_attributeTarget, m_inherited, m_allowMultiple). A miss on m_x retries
* _x, then the table; a final miss is logged. */
static const char *const field_alias [][3] = {
{ NULL, NULL, NULL },
};
U(mono_class_get_field_from_name)
{
ENTER;
MonoClass *k = P (0);
const char *name = P (1);
MonoClassField *f = mono_class_get_field_from_name (k, name);
if (!f && name && name [0] == 'm' && name [1] == '_') {
char *alt = g_strdup (name + 1);
f = mono_class_get_field_from_name (k, alt);
g_free (alt);
}
for (int i = 0; !f && name && field_alias [i][0]; i++)
if (!strcmp (name, field_alias [i][1]) && !strcmp (m_class_get_name (k), field_alias [i][0]))
f = mono_class_get_field_from_name (k, field_alias [i][2]);
if (!f && k)
fprintf (stderr, "[gexmono] no field %s.%s::%s\n", m_class_get_name_space (k), m_class_get_name (k), name ? name : "(null)");
RET (f);
}
/* mono_custom_attrs_from_field (klass, field): Mono finds the field's
* metadata row from its index in klass's field array, so klass must be the
* class that defines the field. UnityPlayer's serializer passes the class it
* is serializing, which for an inherited field is a subclass; use the
* field's own parent. */
U(mono_custom_attrs_from_field)
{
ENTER;
MonoClass *k = P (0);
MonoClassField *f = P (1);
MonoClass *parent = f ? m_field_get_parent (f) : NULL;
if (parent && parent != k) {
static int logged;
if (gexmono_trace_on || logged++ < 8)
fprintf (stderr, "[gexmono] custom_attrs_from_field: %s::%s is %s's, not %s's\n", m_class_get_name (parent),
mono_field_get_name (f), m_class_get_name (parent), k ? m_class_get_name (k) : "?");
k = parent;
}
RET (mono_custom_attrs_from_field (k, f));
}
U(unity_mono_method_is_generic) { ENTER; RET (((MonoMethod *)P (0))->is_generic); }
U(unity_mono_method_is_inflated) { ENTER; RET (((MonoMethod *)P (0))->is_inflated); }
U(unity_mono_reflection_method_get_method) { ENTER; MonoReflectionMethod *m = P (0); RET (m ? m->method : NULL); }
@@ -367,6 +415,6 @@ __attribute__((used)) gexp_body_t const gexmono_unity_adapters [] = {
L(mono_profiler_reset_all_coverage) L(mono_assembly_foreach) L(mono_stack_walk) L(mono_stack_walk_no_il)
L(mono_trace_set_log_handler) L(mono_set_dirs) L(mono_set_assemblies_path) L(mono_set_assemblies_path_null_separated)
L(mono_set_find_plugin_callback) L(mono_dl_fallback_register)
L(unity_mono_method_is_generic) L(unity_mono_method_is_inflated) L(unity_mono_reflection_method_get_method)
L(mono_class_get_field_from_name) L(mono_custom_attrs_from_field) L(unity_mono_method_is_generic) L(unity_mono_method_is_inflated) L(unity_mono_reflection_method_get_method)
0
};