ScriptStack 1.0.5
Loading...
Searching...
No Matches
ClrBridge.cs
Go to the documentation of this file.
1using System;
2using System.Collections;
3using System.Collections.Generic;
4using System.Globalization;
5using System.Linq;
6using System.Reflection;
7
9{
16 public sealed class ClrBridge
17 {
18
19 private readonly IClrPolicy _policy;
20
21 private static readonly BindingFlags ClrMemberFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.IgnoreCase;
22 private static readonly BindingFlags ClrInstanceMemberFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.IgnoreCase;
23 private static readonly BindingFlags ClrStaticMemberFlags = BindingFlags.Static | BindingFlags.Public | BindingFlags.IgnoreCase | BindingFlags.FlattenHierarchy;
24
25 public ClrBridge(IClrPolicy policy)
26 {
27 _policy = policy ?? new DenyAllClrPolicy();
28 }
29
30 internal static object? ScriptNullToClr(object? v) => v is NullReference ? null : v;
31
32 private static object ClrNullToScript(object? v) => v ?? NullReference.Instance;
33
34 private void EnsureTypeAllowed(Type t)
35 {
36 if (!_policy.IsTypeAllowed(t))
37 throw new ExecutionException($"CLR interop is disabled or access denied for type '{t.FullName}'.");
38 }
39
40 private void EnsureReturnAllowed(object? value, string context)
41 {
42 if (!_policy.IsReturnValueAllowed(value))
43 throw new ExecutionException($"CLR interop blocked return value in {context} (type '{value?.GetType().FullName ?? "null"}').");
44 }
45
46 public object GetMember(object target, string memberName) => GetMemberValue(target, memberName);
47 public void SetMember(object target, string memberName, object scriptValue) => SetMemberValue(target, memberName, scriptValue);
48 public bool TryGetIndex(object target, object scriptKey, out object value) => TryGetIndexedValue(target, scriptKey, out value);
49 public bool TrySetIndex(object target, object scriptKey, object scriptValue) => TrySetIndexedValue(target, scriptKey, scriptValue);
50 public object Invoke(object target, string methodName, List<object> args) => InvokeMethod(target, methodName, args);
51
52 // ------------------------
53 // Coercion / Converters
54 // ------------------------
55
56 private static object CoerceTo(object value, Type targetType)
57 {
58 value = ScriptNullToClr(value);
59
60 // Handle Nullable<T>
61 var underlyingNullable = Nullable.GetUnderlyingType(targetType);
62 if (underlyingNullable != null)
63 targetType = underlyingNullable;
64
65 if (value == null)
66 {
67 // null für ValueTypes nicht erlaubt -> Default
68 return targetType.IsValueType ? Activator.CreateInstance(targetType)! : null!;
69 }
70
71 var srcType = value.GetType();
72 if (targetType.IsAssignableFrom(srcType))
73 return value;
74
75 // --- ScriptStack ArrayList -> CLR types ---
76 if (value is ArrayList scriptArr)
77 {
78 // CLR Array (e.g. int[])
79 if (targetType.IsArray)
80 {
81 var elemType = targetType.GetElementType()!;
82 return ConvertScriptArrayListToClrArray(scriptArr, elemType);
83 }
84
85 // Generic collections (List<T>, ICollection<T>, IEnumerable<T>, etc.)
86 if (TryConvertScriptArrayListToGenericCollection(scriptArr, targetType, out var coll))
87 return coll;
88
89 // Generic dictionaries (Dictionary<TKey,TValue>, IDictionary<TKey,TValue>)
90 if (TryConvertScriptArrayListToGenericDictionary(scriptArr, targetType, out var dict))
91 return dict;
92 }
93
94 // Enum conversions
95 if (targetType.IsEnum)
96 return Enum.Parse(targetType, value.ToString()!, ignoreCase: true);
97
98 // numeric / string conversions etc.
99 if (value is IConvertible)
100 return Convert.ChangeType(value, targetType, CultureInfo.InvariantCulture);
101
102 // last resort: keep as-is (Reflection may still accept via implicit operators)
103 return value;
104 }
105
106 private static object ConvertScriptArrayListToClrArray(ArrayList scriptArr, Type elementType)
107 {
108 // We only support list-like arrays here: integer keys, 0..n-1 (no gaps).
109 // Anything associative should be mapped to dictionaries instead.
110 if (scriptArr.Count == 0)
111 return Array.CreateInstance(elementType, 0);
112
113 // ensure all keys are ints
114 var keys = new List<int>(scriptArr.Count);
115 foreach (var k in scriptArr.Keys)
116 {
117 if (k is int i)
118 keys.Add(i);
119 else
120 throw new ExecutionException($"Associatives Array kann nicht zu '{elementType.Name}[]' konvertiert werden (Key-Typ: {k?.GetType().Name ?? "null"}).");
121 }
122
123 keys.Sort();
124 for (int i = 0; i < keys.Count; i++)
125 {
126 if (keys[i] != i)
127 throw new ExecutionException($"Array kann nicht zu '{elementType.Name}[]' konvertiert werden: Keys müssen 0..n-1 ohne Lücken sein.");
128 }
129
130 var arr = Array.CreateInstance(elementType, keys.Count);
131 for (int i = 0; i < keys.Count; i++)
132 {
133 var v = scriptArr[i]; // uses our indexer: returns NullReference.Instance if missing
134 var coerced = CoerceTo(v, elementType);
135 arr.SetValue(coerced, i);
136 }
137 return arr;
138 }
139
140 private static Type? GetGenericInterface(Type type, Type genericDefinition)
141 {
142 if (type.IsInterface && type.IsGenericType && type.GetGenericTypeDefinition() == genericDefinition)
143 return type;
144
145 return type.GetInterfaces()
146 .FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == genericDefinition);
147 }
148
149 private static bool TryConvertScriptArrayListToGenericCollection(ArrayList scriptArr, Type targetType, out object? collection)
150 {
151 collection = null;
152
153 var iface = GetGenericInterface(targetType, typeof(ICollection<>))
154 ?? GetGenericInterface(targetType, typeof(IList<>))
155 ?? GetGenericInterface(targetType, typeof(IEnumerable<>))
156 ?? GetGenericInterface(targetType, typeof(IReadOnlyCollection<>))
157 ?? GetGenericInterface(targetType, typeof(IReadOnlyList<>));
158
159 if (iface == null)
160 return false;
161
162 var elemType = iface.GetGenericArguments()[0];
163
164 // Choose concrete type
165 Type concrete = targetType;
166 if (targetType.IsInterface || targetType.IsAbstract)
167 concrete = typeof(List<>).MakeGenericType(elemType);
168
169 object instance;
170 try
171 {
172 instance = Activator.CreateInstance(concrete)!;
173 }
174 catch
175 {
176 // If the target type is non-instantiable, fallback to List<T>
177 instance = Activator.CreateInstance(typeof(List<>).MakeGenericType(elemType))!;
178 concrete = instance.GetType();
179 }
180
181 // Find Add(T)
182 var add = concrete.GetMethod("Add", new[] { elemType })
183 ?? concrete.GetMethods().FirstOrDefault(m => m.Name == "Add" && m.GetParameters().Length == 1);
184
185 if (add == null)
186 return false;
187
188 // Add items in key order (int keys). If there are non-int keys, refuse.
189 var intKeys = new List<int>();
190 foreach (var k in scriptArr.Keys)
191 {
192 if (k is int i) intKeys.Add(i);
193 else return false;
194 }
195 intKeys.Sort();
196
197 foreach (var k in intKeys)
198 {
199 var v = scriptArr[k];
200 var coerced = CoerceTo(v, elemType);
201 add.Invoke(instance, new[] { coerced });
202 }
203
204 collection = instance;
205 return true;
206 }
207
208 private static bool TryConvertScriptArrayListToGenericDictionary(ArrayList scriptArr, Type targetType, out object? dict)
209 {
210 dict = null;
211
212 var iface = GetGenericInterface(targetType, typeof(IDictionary<,>));
213 if (iface == null)
214 return false;
215
216 var ga = iface.GetGenericArguments();
217 var keyType = ga[0];
218 var valType = ga[1];
219
220 Type concrete = targetType;
221 if (targetType.IsInterface || targetType.IsAbstract)
222 concrete = typeof(Dictionary<,>).MakeGenericType(keyType, valType);
223
224 object instance;
225 try
226 {
227 instance = Activator.CreateInstance(concrete)!;
228 }
229 catch
230 {
231 instance = Activator.CreateInstance(typeof(Dictionary<,>).MakeGenericType(keyType, valType))!;
232 concrete = instance.GetType();
233 }
234
235 var add = concrete.GetMethod("Add", new[] { keyType, valType })
236 ?? concrete.GetMethods().FirstOrDefault(m => m.Name == "Add" && m.GetParameters().Length == 2);
237
238 if (add == null)
239 return false;
240
241 foreach (var k in scriptArr.Keys)
242 {
243 var v = scriptArr[k];
244 var ck = CoerceTo(k, keyType);
245 var cv = CoerceTo(v, valType);
246 add.Invoke(instance, new[] { ck, cv });
247 }
248
249 dict = instance;
250 return true;
251 }
252
253 private static bool TryCoerceTo(object value, Type targetType, out object? coerced)
254 {
255 try
256 {
257 coerced = CoerceTo(value, targetType);
258 return true;
259 }
260 catch
261 {
262 coerced = null;
263 return false;
264 }
265 }
266
267 // ------------------------
268 // Member get/set
269 // ------------------------
270
271 private object GetMemberValue(object target, string memberName)
272 {
273 if (target == null || target is NullReference)
274 return NullReference.Instance;
275
276 bool isStatic = target is Type;
277 var t = isStatic ? (Type)target : target.GetType();
279
280 var flags = isStatic ? ClrStaticMemberFlags : ClrInstanceMemberFlags;
281
282 var field = t.GetField(memberName, flags);
283 if (field != null)
284 {
285 if (!_policy.IsMemberAllowed(field))
286 throw new ExecutionException($"CLR member access denied: '{t.FullName}.{field.Name}'.");
287
288 var v = field.GetValue(isStatic ? null : target);
289 EnsureReturnAllowed(v, $"field '{t.FullName}.{field.Name}'");
290 return v ?? NullReference.Instance;
291 }
292
293 var prop = t.GetProperty(memberName, flags);
294 if (prop != null && prop.CanRead)
295 {
296 if (!_policy.IsMemberAllowed(prop))
297 throw new ExecutionException($"CLR member access denied: '{t.FullName}.{prop.Name}'.");
298
299 var v = prop.GetValue(isStatic ? null : target);
300 EnsureReturnAllowed(v, $"property '{t.FullName}.{prop.Name}'");
301 return v ?? NullReference.Instance;
302 }
303
304 return NullReference.Instance;
305 }
306
307 private void SetMemberValue(object target, string memberName, object scriptValue)
308 {
309 if (target == null || target is NullReference)
310 throw new ExecutionException($"Null reference bei Zuweisung auf Member '{memberName}'.");
311
312 var t = target.GetType();
314
315 var field = t.GetField(memberName, ClrMemberFlags);
316 if (field != null)
317 {
318 if (!_policy.IsMemberAllowed(field))
319 throw new ExecutionException($"CLR member write denied: '{t.FullName}.{field.Name}'.");
320
321 var coerced = CoerceTo(scriptValue, field.FieldType);
322 field.SetValue(target, coerced);
323 return;
324 }
325
326 var prop = t.GetProperty(memberName, ClrMemberFlags);
327 if (prop != null && prop.CanWrite)
328 {
329 if (!_policy.IsMemberAllowed(prop))
330 throw new ExecutionException($"CLR member write denied: '{t.FullName}.{prop.Name}'.");
331
332 var coerced = CoerceTo(scriptValue, prop.PropertyType);
333 prop.SetValue(target, coerced);
334 return;
335 }
336
337 throw new ExecutionException($"Member '{memberName}' nicht gefunden oder nicht schreibbar auf Typ '{t.FullName}'.");
338 }
339
340 // ------------------------
341 // Method invocation
342 // ------------------------
343
344 private object InvokeMethod(object target, string methodName, List<object> args)
345 {
346 if (target == null || target is NullReference)
347 return NullReference.Instance;
348
349 bool isStatic = target is Type;
350 var t = isStatic ? (Type)target : target.GetType();
352
353 var flags = isStatic ? ClrStaticMemberFlags : ClrInstanceMemberFlags;
354 var methods = t.GetMethods(flags);
355
356 MethodInfo? best = null;
357 object[]? bestArgs = null;
358 int bestScore = int.MaxValue;
359
360 bool anyNameMatch = false;
361 bool anyAllowedCandidate = false;
362
363 foreach (var m in methods)
364 {
365 if (!string.Equals(m.Name, methodName, StringComparison.OrdinalIgnoreCase))
366 continue;
367
368 anyNameMatch = true;
369
370 if (!_policy.IsCallAllowed(m))
371 continue;
372
373 anyAllowedCandidate = true;
374
375 var ps = m.GetParameters();
376 if (ps.Length != args.Count)
377 continue;
378
379 int score = 0;
380 var coercedArgs = new object[ps.Length];
381 bool ok = true;
382
383 for (int i = 0; i < ps.Length; i++)
384 {
385 var pType = ps[i].ParameterType;
386 var a = ScriptNullToClr(args[i]);
387
388 if (a == null)
389 {
390 // null passt auf RefTypes/Nullable
391 if (pType.IsValueType && Nullable.GetUnderlyingType(pType) == null)
392 {
393 ok = false;
394 break;
395 }
396
397 coercedArgs[i] = null!;
398 score += 1;
399 continue;
400 }
401
402 var aType = a.GetType();
403
404 if (pType.IsAssignableFrom(aType))
405 {
406 coercedArgs[i] = a;
407 score += (pType == aType) ? 0 : 1;
408 continue;
409 }
410
411 if (TryCoerceTo(a, pType, out var c) && c != null)
412 {
413 coercedArgs[i] = c;
414 score += 2;
415 continue;
416 }
417
418 ok = false;
419 break;
420 }
421
422 if (!ok)
423 continue;
424
425 if (score < bestScore)
426 {
427 bestScore = score;
428 best = m;
429 bestArgs = coercedArgs;
430 }
431 }
432
433 if (best == null)
434 {
435 if (anyNameMatch && !anyAllowedCandidate)
436 throw new ExecutionException($"CLR method call denied: '{t.FullName}.{methodName}(...)'.");
437
438 throw new ExecutionException($"Methode '{methodName}' mit {args.Count} Parametern nicht gefunden auf Typ '{t.FullName}'.");
439 }
440
441 try
442 {
443
444 object? result = best.Invoke(isStatic ? null : target, bestArgs);
445 if (best.ReturnType == typeof(void))
446 return NullReference.Instance;
447
448 EnsureReturnAllowed(result, $"method '{t.FullName}.{best.Name}'");
449 return ClrNullToScript(result);
450 }
451 catch (TargetInvocationException tie)
452 {
453 // unwrap inner exception for better diagnostics
454 throw new ExecutionException($"Fehler in CLR-Methode '{t.FullName}.{best.Name}': {tie.InnerException?.Message ?? tie.Message}");
455 }
456 catch (Exception ex)
457 {
458 throw new ExecutionException($"Fehler beim Aufruf von CLR-Methode '{t.FullName}.{best.Name}': {ex.Message}");
459 }
460 }
461
462 // ------------------------
463 // Index get/set
464 // ------------------------
465
466 private bool TryGetIndexedValue(object target, object scriptKey, out object value)
467 {
468 value = NullReference.Instance;
469
470 if (target == null || target is NullReference)
471 return true;
472
473 var t = target.GetType();
475
476 // IList / arrays
477 if (target is IList list)
478 {
479 var idxObj = ScriptNullToClr(scriptKey);
480 if (idxObj is int idx)
481 {
482 var v = list[idx];
483 EnsureReturnAllowed(v, $"index get '{t.FullName}[{idx}]'");
484 value = ClrNullToScript(v);
485 return true;
486 }
487
488 return false;
489 }
490
491 // IDictionary
492 if (target is IDictionary dict)
493 {
494 object? key = ScriptNullToClr(scriptKey);
495
496 // Try to coerce key to generic TKey if possible (avoids InvalidCastException)
497 var keyType = target.GetType().GetInterfaces()
498 .FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IDictionary<,>))
499 ?.GetGenericArguments()[0];
500
501 if (keyType != null && key != null && TryCoerceTo(key, keyType, out var ck) && ck != null)
502 key = ck;
503
504 try
505 {
506 var v = dict[key!];
507 EnsureReturnAllowed(v, $"dictionary get '{t.FullName}[key]'");
508 value = ClrNullToScript(v);
509 return true;
510 }
511 catch
512 {
513 // fall through to indexer reflection
514 }
515 }
516
517 // indexer property (Item[...])
518 foreach (var p in t.GetProperties(ClrMemberFlags))
519 {
520 var ip = p.GetIndexParameters();
521 if (ip.Length != 1 || !p.CanRead)
522 continue;
523
524 if (!_policy.IsMemberAllowed(p))
525 continue;
526
527 if (!TryCoerceTo(scriptKey, ip[0].ParameterType, out var ck))
528 continue;
529
530 try
531 {
532 var v = p.GetValue(target, new object[] { ck });
533 EnsureReturnAllowed(v, $"indexer get '{t.FullName}.{p.Name}[...]' ");
534 value = ClrNullToScript(v);
535 return true;
536 }
537 catch
538 {
539 // try next
540 }
541 }
542
543 return false;
544 }
545
546 private bool TrySetIndexedValue(object target, object scriptKey, object scriptValue)
547 {
548 if (target == null || target is NullReference)
549 return false;
550
551 var t = target.GetType();
553
554 // IList / arrays
555 if (target is IList list)
556 {
557 var idxObj = ScriptNullToClr(scriptKey);
558 if (idxObj is not int idx)
559 return false;
560
561 // try to coerce value to element type if we can infer it
562 Type? elemType = null;
563 var tt = target.GetType();
564 if (tt.IsArray)
565 elemType = tt.GetElementType();
566 else
567 {
568 var gi = tt.GetInterfaces().FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IList<>));
569 if (gi != null) elemType = gi.GetGenericArguments()[0];
570 }
571
572 object v = scriptValue;
573 if (elemType != null && TryCoerceTo(scriptValue, elemType, out var cv) && cv != null)
574 v = cv;
575
576 list[idx] = ScriptNullToClr(v);
577 return true;
578 }
579
580 // IDictionary
581 if (target is IDictionary dict)
582 {
583 object? key = ScriptNullToClr(scriptKey);
584 object? val = ScriptNullToClr(scriptValue);
585
586 var iface = target.GetType().GetInterfaces()
587 .FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IDictionary<,>));
588 if (iface != null)
589 {
590 var ga = iface.GetGenericArguments();
591 var keyType = ga[0];
592 var valType = ga[1];
593 if (key != null && TryCoerceTo(key, keyType, out var ck) && ck != null) key = ck;
594 if (val != null && TryCoerceTo(val, valType, out var cv) && cv != null) val = cv;
595 }
596
597 try
598 {
599 dict[key!] = val;
600 return true;
601 }
602 catch
603 {
604 // fall through to indexer reflection
605 }
606 }
607
608 // indexer property (Item[...])
609 foreach (var p in t.GetProperties(ClrMemberFlags))
610 {
611 var ip = p.GetIndexParameters();
612 if (ip.Length != 1 || !p.CanWrite)
613 continue;
614
615 if (!_policy.IsMemberAllowed(p))
616 continue;
617
618 if (!TryCoerceTo(scriptKey, ip[0].ParameterType, out var ck))
619 continue;
620
621 object v = scriptValue;
622 if (TryCoerceTo(scriptValue, p.PropertyType, out var cv) && cv != null)
623 v = cv;
624
625 try
626 {
627 p.SetValue(target, ScriptNullToClr(v), new object[] { ck });
628 return true;
629 }
630 catch
631 {
632 // try next
633 }
634 }
635
636 return false;
637 }
638
639 }
640}
bool TrySetIndex(object target, object scriptKey, object scriptValue)
static bool TryCoerceTo(object value, Type targetType, out object? coerced)
Definition ClrBridge.cs:253
object GetMember(object target, string memberName)
void SetMember(object target, string memberName, object scriptValue)
static object CoerceTo(object value, Type targetType)
Definition ClrBridge.cs:56
static bool TryConvertScriptArrayListToGenericCollection(ArrayList scriptArr, Type targetType, out object? collection)
Definition ClrBridge.cs:149
static readonly BindingFlags ClrInstanceMemberFlags
Definition ClrBridge.cs:22
static readonly BindingFlags ClrMemberFlags
Definition ClrBridge.cs:21
object GetMemberValue(object target, string memberName)
Definition ClrBridge.cs:271
object Invoke(object target, string methodName, List< object > args)
static object ConvertScriptArrayListToClrArray(ArrayList scriptArr, Type elementType)
Definition ClrBridge.cs:106
static object ClrNullToScript(object? v)
ClrBridge(IClrPolicy policy)
Definition ClrBridge.cs:25
bool TryGetIndexedValue(object target, object scriptKey, out object value)
Definition ClrBridge.cs:466
static readonly BindingFlags ClrStaticMemberFlags
Definition ClrBridge.cs:23
static bool TryConvertScriptArrayListToGenericDictionary(ArrayList scriptArr, Type targetType, out object? dict)
Definition ClrBridge.cs:208
static ? Type GetGenericInterface(Type type, Type genericDefinition)
Definition ClrBridge.cs:140
bool TrySetIndexedValue(object target, object scriptKey, object scriptValue)
Definition ClrBridge.cs:546
bool TryGetIndex(object target, object scriptKey, out object value)
readonly IClrPolicy _policy
Definition ClrBridge.cs:19
void SetMemberValue(object target, string memberName, object scriptValue)
Definition ClrBridge.cs:307
object InvokeMethod(object target, string methodName, List< object > args)
Definition ClrBridge.cs:344
void EnsureReturnAllowed(object? value, string context)
Definition ClrBridge.cs:40
Denies all CLR interop. Default Policy!
Policy hook for CLR interop. Implementations decide what is accessible from scripts.
Definition IClrPolicy.cs:10