Skip to content

Commit 9418913

Browse files
authored
Merge pull request #87 from MerlinVR/variable-callbacks
Variable callbacks
2 parents 14e56ce + 3a50ff7 commit 9418913

10 files changed

Lines changed: 248 additions & 48 deletions

Assets/UdonSharp/Editor/UdonSharpASTVisitor.cs

Lines changed: 14 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ public class ASTVisitorContext
2626
public List<MethodDefinition> definedMethods;
2727

2828
public List<PropertyDefinition> definedProperties;
29+
public Dictionary<string, FieldDefinition> onModifyCallbackFields = new Dictionary<string, FieldDefinition>();
2930

3031
// Tracking labels for the current function and flow control
3132
public JumpLabel returnLabel = null;
@@ -531,6 +532,19 @@ public override void VisitPropertyDeclaration(PropertyDeclarationSyntax node)
531532
{
532533
var setter = definition.setter;
533534

535+
// Handle VRC field modification callbacks
536+
if (visitorContext.onModifyCallbackFields.TryGetValue(definition.originalPropertyName, out FieldDefinition targetField))
537+
{
538+
string exportStr = VRC.Udon.Common.VariableChangedEvent.EVENT_PREFIX + targetField.fieldSymbol.symbolUniqueName;
539+
visitorContext.uasmBuilder.AppendLine($".export {exportStr}", 1);
540+
visitorContext.uasmBuilder.AppendLine($"{exportStr}:", 1);
541+
542+
SymbolDefinition oldPropertyVal = visitorContext.topTable.GetGlobalSymbolTable().CreateNamedSymbol($"{VRC.Udon.Common.VariableChangedEvent.OLD_VALUE_PREFIX}{targetField.fieldSymbol.symbolUniqueName}", targetField.fieldSymbol.userCsType, SymbolDeclTypeFlags.Private);
543+
544+
visitorContext.uasmBuilder.AddCopy(setter.paramSymbol, targetField.fieldSymbol);
545+
visitorContext.uasmBuilder.AddCopy(targetField.fieldSymbol, oldPropertyVal);
546+
}
547+
534548
if ((node.Modifiers.HasModifier("public") && setter.declarationFlags == PropertyDeclFlags.None) || setter.declarationFlags == PropertyDeclFlags.Public)
535549
{
536550
visitorContext.uasmBuilder.AppendLine($".export {setter.accessorName}", 1);
@@ -2473,41 +2487,6 @@ public override void VisitSwitchStatement(SwitchStatementSyntax node)
24732487
visitorContext.breakLabelStack.Pop();
24742488
}
24752489

2476-
private void HandleNameOfExpression(InvocationExpressionSyntax node)
2477-
{
2478-
SyntaxNode currentNode = node.ArgumentList.Arguments[0].Expression;
2479-
string currentName = "";
2480-
2481-
while (currentNode != null)
2482-
{
2483-
switch (currentNode.Kind())
2484-
{
2485-
case SyntaxKind.SimpleMemberAccessExpression:
2486-
MemberAccessExpressionSyntax memberNode = (MemberAccessExpressionSyntax)currentNode;
2487-
currentName = memberNode.Name.ToString();
2488-
currentNode = memberNode.Name;
2489-
break;
2490-
case SyntaxKind.IdentifierName:
2491-
IdentifierNameSyntax identifierName = (IdentifierNameSyntax)currentNode;
2492-
currentName = identifierName.ToString();
2493-
currentNode = null;
2494-
break;
2495-
default:
2496-
currentNode = null;
2497-
break;
2498-
}
2499-
2500-
if (currentNode != null)
2501-
UpdateSyntaxNode(currentNode);
2502-
}
2503-
2504-
if (currentName == "")
2505-
throw new System.ArgumentException("Expression does not have a name");
2506-
2507-
if (visitorContext.topCaptureScope != null)
2508-
visitorContext.topCaptureScope.SetToLocalSymbol(visitorContext.topTable.CreateConstSymbol(typeof(string), currentName));
2509-
}
2510-
25112490
public override void VisitCaseSwitchLabel(CaseSwitchLabelSyntax node)
25122491
{
25132492
UpdateSyntaxNode(node);

Assets/UdonSharp/Editor/UdonSharpCompilationModule.cs

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -86,15 +86,15 @@ public CompileTaskResult Compile(List<ClassDefinition> classDefinitions, Microso
8686
debugInfo = new ClassDebugInfo(sourceCode, settings == null || settings.includeInlineCode);
8787
}
8888

89-
UdonSharpFieldVisitor fieldVisitor = new UdonSharpFieldVisitor(fieldsWithInitializers, resolver, moduleSymbols, moduleLabels, classDefinitions, debugInfo);
90-
89+
PropertyVisitor propertyVisitor = new PropertyVisitor(resolver, moduleSymbols, moduleLabels);
90+
9191
try
9292
{
93-
fieldVisitor.Visit(syntaxTree.GetRoot());
93+
propertyVisitor.Visit(syntaxTree.GetRoot());
9494
}
9595
catch (System.Exception e)
9696
{
97-
LogException(result, e, fieldVisitor.visitorContext.currentNode, out string logMessage);
97+
LogException(result, e, propertyVisitor.visitorContext.currentNode, out string logMessage);
9898

9999
programAsset.compileErrors.Add(logMessage);
100100

@@ -104,15 +104,16 @@ public CompileTaskResult Compile(List<ClassDefinition> classDefinitions, Microso
104104
if (ErrorCount > 0)
105105
return result;
106106

107-
MethodVisitor methodVisitor = new MethodVisitor(resolver, moduleSymbols, moduleLabels);
107+
UdonSharpFieldVisitor fieldVisitor = new UdonSharpFieldVisitor(fieldsWithInitializers, resolver, moduleSymbols, moduleLabels, classDefinitions, debugInfo);
108+
fieldVisitor.visitorContext.definedProperties = propertyVisitor.definedProperties;
108109

109110
try
110111
{
111-
methodVisitor.Visit(syntaxTree.GetRoot());
112+
fieldVisitor.Visit(syntaxTree.GetRoot());
112113
}
113114
catch (System.Exception e)
114115
{
115-
LogException(result, e, methodVisitor.visitorContext.currentNode, out string logMessage);
116+
LogException(result, e, fieldVisitor.visitorContext.currentNode, out string logMessage);
116117

117118
programAsset.compileErrors.Add(logMessage);
118119

@@ -122,15 +123,15 @@ public CompileTaskResult Compile(List<ClassDefinition> classDefinitions, Microso
122123
if (ErrorCount > 0)
123124
return result;
124125

125-
PropertyVisitor propertyVisitor = new PropertyVisitor(resolver, moduleSymbols, moduleLabels);
126+
MethodVisitor methodVisitor = new MethodVisitor(resolver, moduleSymbols, moduleLabels);
126127

127128
try
128129
{
129-
propertyVisitor.Visit(syntaxTree.GetRoot());
130+
methodVisitor.Visit(syntaxTree.GetRoot());
130131
}
131132
catch (System.Exception e)
132133
{
133-
LogException(result, e, propertyVisitor.visitorContext.currentNode, out string logMessage);
134+
LogException(result, e, methodVisitor.visitorContext.currentNode, out string logMessage);
134135

135136
programAsset.compileErrors.Add(logMessage);
136137

@@ -141,6 +142,7 @@ public CompileTaskResult Compile(List<ClassDefinition> classDefinitions, Microso
141142
return result;
142143

143144
ASTVisitor visitor = new ASTVisitor(resolver, moduleSymbols, moduleLabels, methodVisitor.definedMethods, propertyVisitor.definedProperties, classDefinitions, debugInfo);
145+
visitor.visitorContext.onModifyCallbackFields = fieldVisitor.visitorContext.onModifyCallbackFields;
144146

145147
try
146148
{

Assets/UdonSharp/Editor/UdonSharpExpressionCapture.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -740,6 +740,9 @@ public void ExecuteSet(SymbolDefinition value, bool explicitCast = false)
740740
}
741741
else if (captureArchetype == ExpressionCaptureArchetype.ExternUserField)
742742
{
743+
if (visitorContext.onModifyCallbackFields.Values.Any(e => e.fieldSymbol.symbolUniqueName == captureExternUserField.fieldSymbol.symbolUniqueName))
744+
throw new System.InvalidOperationException($"Cannot set field with {nameof(FieldChangeCallbackAttribute)}, use a property or SetProgramVariable");
745+
743746
using (ExpressionCaptureScope setVariableMethodScope = new ExpressionCaptureScope(visitorContext, null))
744747
{
745748
setVariableMethodScope.SetToLocalSymbol(accessSymbol);

Assets/UdonSharp/Editor/UdonSharpFieldVisitor.cs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using Microsoft.CodeAnalysis.CSharp;
44
using Microsoft.CodeAnalysis.CSharp.Syntax;
55
using System.Collections.Generic;
6+
using System.Linq;
67
using UnityEngine;
78

89
namespace UdonSharp.Compiler
@@ -49,11 +50,14 @@ public override void VisitFieldDeclaration(FieldDeclarationSyntax node)
4950

5051
List<System.Attribute> fieldAttributes = GetFieldAttributes(node);
5152

53+
5254
bool isPublic = (node.Modifiers.Any(SyntaxKind.PublicKeyword) || fieldAttributes.Find(e => e is SerializeField) != null) && fieldAttributes.Find(e => e is System.NonSerializedAttribute) == null;
5355
bool isConst = (node.Modifiers.Any(SyntaxKind.ConstKeyword) || node.Modifiers.Any(SyntaxKind.ReadOnlyKeyword));
5456
SymbolDeclTypeFlags flags = (isPublic ? SymbolDeclTypeFlags.Public : SymbolDeclTypeFlags.Private) |
5557
(isConst ? SymbolDeclTypeFlags.Readonly : 0);
5658

59+
FieldChangeCallbackAttribute varChange = fieldAttributes.OfType<FieldChangeCallbackAttribute>().FirstOrDefault();
60+
5761
List<SymbolDefinition> fieldSymbols = HandleVariableDeclaration(node.Declaration, flags, fieldSyncMode);
5862
foreach (SymbolDefinition fieldSymbol in fieldSymbols)
5963
{
@@ -77,6 +81,28 @@ public override void VisitFieldDeclaration(FieldDeclarationSyntax node)
7781
}
7882

7983
visitorContext.localFieldDefinitions.Add(fieldSymbol.symbolUniqueName, fieldDefinition);
84+
85+
if (varChange != null)
86+
{
87+
string targetProperty = varChange.CallbackPropertyName;
88+
89+
if (variables.Count > 1 || visitorContext.onModifyCallbackFields.ContainsKey(targetProperty))
90+
throw new System.Exception($"Only one field may target property '{targetProperty}'");
91+
92+
PropertyDefinition foundProperty = visitorContext.definedProperties.FirstOrDefault(e => e.originalPropertyName == targetProperty);
93+
94+
if (foundProperty == null)
95+
throw new System.ArgumentException($"Invalid target property for {nameof(FieldChangeCallbackAttribute)} on {node.Declaration}");
96+
97+
PropertyDefinition property = visitorContext.definedProperties.FirstOrDefault(e => e.originalPropertyName == targetProperty);
98+
if (property == null)
99+
throw new System.ArgumentException($"Property not found for '{targetProperty}'");
100+
101+
if (property.type != fieldDefinition.fieldSymbol.userCsType)
102+
throw new System.Exception($"Types must match between property and variable change field");
103+
104+
visitorContext.onModifyCallbackFields.Add(targetProperty, fieldDefinition);
105+
}
80106
}
81107
}
82108

Assets/UdonSharp/Editor/UdonSharpSyntaxWalker.cs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,52 @@ public override void VisitUsingDirective(UsingDirectiveSyntax node)
319319
}
320320
}
321321

322+
protected void HandleNameOfExpression(InvocationExpressionSyntax node)
323+
{
324+
SyntaxNode currentNode = node.ArgumentList.Arguments[0].Expression;
325+
string currentName = "";
326+
327+
while (currentNode != null)
328+
{
329+
switch (currentNode.Kind())
330+
{
331+
case SyntaxKind.SimpleMemberAccessExpression:
332+
MemberAccessExpressionSyntax memberNode = (MemberAccessExpressionSyntax)currentNode;
333+
currentName = memberNode.Name.ToString();
334+
currentNode = memberNode.Name;
335+
break;
336+
case SyntaxKind.IdentifierName:
337+
IdentifierNameSyntax identifierName = (IdentifierNameSyntax)currentNode;
338+
currentName = identifierName.ToString();
339+
currentNode = null;
340+
break;
341+
default:
342+
currentNode = null;
343+
break;
344+
}
345+
346+
if (currentNode != null)
347+
UpdateSyntaxNode(currentNode);
348+
}
349+
350+
if (currentName == "")
351+
throw new System.ArgumentException("Expression does not have a name");
352+
353+
if (visitorContext.topCaptureScope != null)
354+
visitorContext.topCaptureScope.SetToLocalSymbol(visitorContext.topTable.CreateConstSymbol(typeof(string), currentName));
355+
}
356+
357+
public override void VisitInvocationExpression(InvocationExpressionSyntax node)
358+
{
359+
UpdateSyntaxNode(node);
360+
361+
if (node.Expression != null && node.Expression.ToString() == "nameof") // nameof is not a dedicated node and the Kind of the node isn't the nameof kind for whatever reason...
362+
{
363+
HandleNameOfExpression(node);
364+
return;
365+
}
366+
}
367+
322368
public override void VisitIdentifierName(IdentifierNameSyntax node)
323369
{
324370
UpdateSyntaxNode(node);

Assets/UdonSharp/Scripts/UdonSharpAttributes.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,5 +89,23 @@ public class RecursiveMethodAttribute : Attribute
8989
public RecursiveMethodAttribute()
9090
{ }
9191
}
92+
93+
/// <summary>
94+
/// Calls the target property's setter when the marked field is modified by network sync or SetProgramVariable().
95+
/// Fields marked with this will instead have the target property's setter called. The setter is expected to set the field if you want the field to change.
96+
/// </summary>
97+
[PublicAPI]
98+
[AttributeUsage(AttributeTargets.Field, AllowMultiple = false)]
99+
public class FieldChangeCallbackAttribute : Attribute
100+
{
101+
public string CallbackPropertyName { get; private set; }
102+
103+
private FieldChangeCallbackAttribute() { }
104+
105+
public FieldChangeCallbackAttribute(string targetPropertyName)
106+
{
107+
CallbackPropertyName = targetPropertyName;
108+
}
109+
}
92110
}
93111

Assets/UdonSharp/Scripts/UdonSharpBehaviour.cs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,26 @@ public void SetProgramVariable(string name, object value)
3434

3535
if (variableField != null)
3636
{
37-
variableField.SetValue(this, value);
37+
FieldChangeCallbackAttribute fieldChangeCallback = variableField.GetCustomAttribute<FieldChangeCallbackAttribute>();
38+
39+
if (fieldChangeCallback != null)
40+
{
41+
PropertyInfo targetProperty = variableField.DeclaringType.GetProperty(fieldChangeCallback.CallbackPropertyName, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance);
42+
43+
if (targetProperty == null)
44+
return;
45+
46+
MethodInfo setMethod = targetProperty.GetSetMethod(true);
47+
48+
if (setMethod == null)
49+
return;
50+
51+
setMethod.Invoke(this, new object[] { value });
52+
}
53+
else
54+
{
55+
variableField.SetValue(this, value);
56+
}
3857
}
3958
}
4059

Assets/UdonSharp/Tests/IntegrationTestScene.unity

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8356,7 +8356,7 @@ MonoBehaviour:
83568356
serializedProgramAsset: {fileID: 11400000, guid: c341eeb82008276418f85fd69d4dd8b3,
83578357
type: 2}
83588358
programSource: {fileID: 11400000, guid: e6b13e0cbf910824dbf27af03fe07b7f, type: 2}
8359-
serializedPublicVariablesBytesString: Ai8AAAAAATIAAABWAFIAQwAuAFUAZABvAG4ALgBDAG8AbQBtAG8AbgAuAFUAZABvAG4AVgBhAHIAaQBhAGIAbABlAFQAYQBiAGwAZQAsACAAVgBSAEMALgBVAGQAbwBuAC4AQwBvAG0AbQBvAG4AAAAAAAYBAAAAAAAAACcBBAAAAHQAeQBwAGUAAWgAAABTAHkAcwB0AGUAbQAuAEMAbwBsAGwAZQBjAHQAaQBvAG4AcwAuAEcAZQBuAGUAcgBpAGMALgBMAGkAcwB0AGAAMQBbAFsAVgBSAEMALgBVAGQAbwBuAC4AQwBvAG0AbQBvAG4ALgBJAG4AdABlAHIAZgBhAGMAZQBzAC4ASQBVAGQAbwBuAFYAYQByAGkAYQBiAGwAZQAsACAAVgBSAEMALgBVAGQAbwBuAC4AQwBvAG0AbQBvAG4AXQBdACwAIABtAHMAYwBvAHIAbABpAGIAAQEJAAAAVgBhAHIAaQBhAGIAbABlAHMALwEAAAABaAAAAFMAeQBzAHQAZQBtAC4AQwBvAGwAbABlAGMAdABpAG8AbgBzAC4ARwBlAG4AZQByAGkAYwAuAEwAaQBzAHQAYAAxAFsAWwBWAFIAQwAuAFUAZABvAG4ALgBDAG8AbQBtAG8AbgAuAEkAbgB0AGUAcgBmAGEAYwBlAHMALgBJAFUAZABvAG4AVgBhAHIAaQBhAGIAbABlACwAIABWAFIAQwAuAFUAZABvAG4ALgBDAG8AbQBtAG8AbgBdAF0ALAAgAG0AcwBjAG8AcgBsAGkAYgABAAAABgAAAAAAAAAABwUHBQ==
8359+
serializedPublicVariablesBytesString: Ai8AAAAAATIAAABWAFIAQwAuAFUAZABvAG4ALgBDAG8AbQBtAG8AbgAuAFUAZABvAG4AVgBhAHIAaQBhAGIAbABlAFQAYQBiAGwAZQAsACAAVgBSAEMALgBVAGQAbwBuAC4AQwBvAG0AbQBvAG4AAAAAAAYBAAAAAAAAACcBBAAAAHQAeQBwAGUAAWgAAABTAHkAcwB0AGUAbQAuAEMAbwBsAGwAZQBjAHQAaQBvAG4AcwAuAEcAZQBuAGUAcgBpAGMALgBMAGkAcwB0AGAAMQBbAFsAVgBSAEMALgBVAGQAbwBuAC4AQwBvAG0AbQBvAG4ALgBJAG4AdABlAHIAZgBhAGMAZQBzAC4ASQBVAGQAbwBuAFYAYQByAGkAYQBiAGwAZQAsACAAVgBSAEMALgBVAGQAbwBuAC4AQwBvAG0AbQBvAG4AXQBdACwAIABtAHMAYwBvAHIAbABpAGIAAQEJAAAAVgBhAHIAaQBhAGIAbABlAHMALwEAAAABaAAAAFMAeQBzAHQAZQBtAC4AQwBvAGwAbABlAGMAdABpAG8AbgBzAC4ARwBlAG4AZQByAGkAYwAuAEwAaQBzAHQAYAAxAFsAWwBWAFIAQwAuAFUAZABvAG4ALgBDAG8AbQBtAG8AbgAuAEkAbgB0AGUAcgBmAGEAYwBlAHMALgBJAFUAZABvAG4AVgBhAHIAaQBhAGIAbABlACwAIABWAFIAQwAuAFUAZABvAG4ALgBDAG8AbQBtAG8AbgBdAF0ALAAgAG0AcwBjAG8AcgBsAGkAYgABAAAABgEAAAAAAAAAAi8CAAAAAUoAAABWAFIAQwAuAFUAZABvAG4ALgBDAG8AbQBtAG8AbgAuAFUAZABvAG4AVgBhAHIAaQBhAGIAbABlAGAAMQBbAFsAUwB5AHMAdABlAG0ALgBTAGkAbgBnAGwAZQAsACAAbQBzAGMAbwByAGwAaQBiAF0AXQAsACAAVgBSAEMALgBVAGQAbwBuAC4AQwBvAG0AbQBvAG4AAgAAAAYCAAAAAAAAACcBBAAAAHQAeQBwAGUAARcAAABTAHkAcwB0AGUAbQAuAFMAdAByAGkAbgBnACwAIABtAHMAYwBvAHIAbABpAGIAJwEKAAAAUwB5AG0AYgBvAGwATgBhAG0AZQABFwAAAGIAYQBjAGsAaQBuAGcAQwBhAGwAbABiAGEAYwBrAFAAcgBvAHAAZQByAHQAeQAnAQQAAAB0AHkAcABlAAEXAAAAUwB5AHMAdABlAG0ALgBTAGkAbgBnAGwAZQAsACAAbQBzAGMAbwByAGwAaQBiAB8BBQAAAFYAYQBsAHUAZQAAAAAABwUHBQcF
83608360
publicVariablesUnityEngineObjects: []
83618361
publicVariablesSerializationDataFormat: 0
83628362
--- !u!4 &1954857073

Assets/UdonSharp/Tests/TestScripts/Core/PropertyTest.asset

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ MonoBehaviour:
4444
Data:
4545
- Name:
4646
Entry: 12
47-
Data: 4
47+
Data: 5
4848
- Name:
4949
Entry: 7
5050
Data:
@@ -297,6 +297,69 @@ MonoBehaviour:
297297
- Name:
298298
Entry: 8
299299
Data:
300+
- Name:
301+
Entry: 7
302+
Data:
303+
- Name: $k
304+
Entry: 1
305+
Data: backingCallbackfield
306+
- Name: $v
307+
Entry: 7
308+
Data: 18|UdonSharp.Compiler.FieldDefinition, UdonSharp.Editor
309+
- Name: fieldSymbol
310+
Entry: 7
311+
Data: 19|UdonSharp.Compiler.SymbolDefinition, UdonSharp.Editor
312+
- Name: internalType
313+
Entry: 9
314+
Data: 9
315+
- Name: declarationType
316+
Entry: 3
317+
Data: 1
318+
- Name: syncMode
319+
Entry: 3
320+
Data: 0
321+
- Name: symbolResolvedTypeName
322+
Entry: 1
323+
Data: SystemSingle
324+
- Name: symbolOriginalName
325+
Entry: 1
326+
Data: backingCallbackfield
327+
- Name: symbolUniqueName
328+
Entry: 1
329+
Data: backingCallbackfield
330+
- Name: symbolDefaultValue
331+
Entry: 6
332+
Data:
333+
- Name:
334+
Entry: 8
335+
Data:
336+
- Name: fieldAttributes
337+
Entry: 7
338+
Data: 20|System.Collections.Generic.List`1[[System.Attribute, mscorlib]], mscorlib
339+
- Name:
340+
Entry: 12
341+
Data: 1
342+
- Name:
343+
Entry: 7
344+
Data: 21|UdonSharp.FieldChangeCallbackAttribute, UdonSharp.Runtime
345+
- Name:
346+
Entry: 8
347+
Data:
348+
- Name:
349+
Entry: 13
350+
Data:
351+
- Name:
352+
Entry: 8
353+
Data:
354+
- Name: userBehaviourSource
355+
Entry: 6
356+
Data:
357+
- Name:
358+
Entry: 8
359+
Data:
360+
- Name:
361+
Entry: 8
362+
Data:
300363
- Name:
301364
Entry: 13
302365
Data:

0 commit comments

Comments
 (0)