Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions corlib/MenSharpBehaviour.Unity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,30 @@ public class MenSharpBehaviour

public UnityEngine.Transform transform { get; }

private System.Threading.CancellationTokenSource destroyTokenSource;

public System.Threading.CancellationToken destroyCancellationToken
{
get
{
// Allocate the source lazily, as Unity does.
if (destroyTokenSource == null)
{
destroyTokenSource = new System.Threading.CancellationTokenSource();
}
return destroyTokenSource.Token;
}
}

// Called before the user's OnDestroy by generated code.
internal void __CancelDestroyToken()
{
if (destroyTokenSource != null)
{
destroyTokenSource.Cancel();
}
}

// The program itself. Declared as UdonBehaviour rather than as the
// interface its methods live on, because a `this` heap reference may
// only be a GameObject, a Transform or an UdonBehaviour — Udon refuses
Expand Down
97 changes: 86 additions & 11 deletions src/men-sharp-codegen/src/generator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,8 @@ struct FieldCallback {
struct EventEntry {
name: String,
key: FunctionKey,
/// Pre-hooks, in execution order, before the event body.
before: Vec<FunctionKey>,
arguments: Vec<EventArgument>,
/// `OnOwnershipRequest`: Udon reads the result back from `__returnValue`.
returns_value: bool,
Expand Down Expand Up @@ -982,6 +984,7 @@ impl<'a, 'ast> Generator<'a, 'ast> {
entries.push(EventEntry {
name,
key,
before: Vec::new(),
arguments,
// the one event whose *result* Udon reads back, from
// `__returnValue` (UdonSharp does the same copy)
Expand All @@ -1004,6 +1007,7 @@ impl<'a, 'ast> Generator<'a, 'ast> {
}
}
}
self.add_destroy_token_handler(&mut entries, &mut claimed);
// `[FieldChangeCallback]` fields each get an `_onVarChange_…` entry;
// collected before the queue drains so their setters get compiled
let callbacks = self.collect_field_callbacks();
Expand Down Expand Up @@ -1057,6 +1061,7 @@ impl<'a, 'ast> Generator<'a, 'ast> {
entries.push(EventEntry {
name: delegates::REMOTE_INVOKE_EVENT.to_string(),
key,
before: Vec::new(),
arguments: Vec::new(),
returns_value: false,
result_slot: None,
Expand Down Expand Up @@ -1085,6 +1090,22 @@ impl<'a, 'ast> Generator<'a, 'ast> {
let key = &entry.key;
self.begin_entry_stub(name, init_label, init_return, initialized);

for (index, before) in entry.before.iter().enumerate() {
let function = &self.functions[before];
let (return_slot, label) = (function.return_slot, function.label);
// Resume at the next hook, or the event body, after this call.
let done = self
.program
.add_label(format!("event_{name}__before_{index}_done"));
let return_to_stub =
self.code_address_constant(format!("__ret_before_{name}_{index}"), Some(done));
self.copy(return_to_stub, return_slot);
self.program.code.push(Op::Jump(Target::Label(label)));
self.program.code.push(Op::Label(done));
// Do not continue into the event after an uncaught hook error.
self.emit_unhandled_check(name);
}

// the event's arguments: the runtime wrote them into the named
// slots before raising the event; hand them to the function
let function = &self.functions[key];
Expand Down Expand Up @@ -2795,6 +2816,7 @@ impl<'a, 'ast> Generator<'a, 'ast> {
entries.push(EventEntry {
name,
key,
before: Vec::new(),
arguments,
returns_value: false,
result_slot: None,
Expand All @@ -2804,6 +2826,63 @@ impl<'a, 'ast> Generator<'a, 'ast> {
added
}

/// Adds MenSharpBehaviour's cancellation hook to Udon's `OnDestroy`
/// event, without taking the event away from a user-defined handler.
fn add_destroy_token_handler(
&mut self,
entries: &mut Vec<EventEntry>,
claimed: &mut HashSet<String>,
) {
let Some(marker) = self.marker else {
return;
};
// The marker owns the hook; the source subclass owns `OnDestroy`.
let Some(method) = self
.declarations
.table
.symbol(marker)
.members_named("__CancelDestroyToken")
.iter()
.copied()
.find(|&member| self.declarations.table.symbol(member).kind == SymbolKind::Method)
else {
return;
};
let key = FunctionKey {
symbol: method,
role: Role::Method,
bindings: Vec::new(),
};
self.add_before_handler(entries, claimed, udon_event_name("OnDestroy"), key);
}

/// Adds a pre-hook, or creates a synthetic entry when no body exists.
fn add_before_handler(
&mut self,
entries: &mut Vec<EventEntry>,
claimed: &mut HashSet<String>,
name: String,
key: FunctionKey,
) {
// Compile the hook even when it is the synthetic event body.
self.ensure_function(&key);
if let Some(entry) = entries.iter_mut().find(|entry| entry.name == name) {
// Keep registration order so hooks run deterministically.
entry.before.push(key);
return;
}
// With no user body, the first hook is the entry's main function.
claimed.insert(name.clone());
entries.push(EventEntry {
name,
key,
before: Vec::new(),
arguments: Vec::new(),
returns_value: false,
result_slot: None,
});
}

fn find_symbol(&self, path: &[&str]) -> Option<SymbolId> {
let mut current = self.declarations.table.root();
for segment in path {
Expand Down Expand Up @@ -3978,15 +4057,7 @@ impl<'a, 'ast> Generator<'a, 'ast> {
if self.is_bodiless(symbol) {
return false;
}
let entry = self.declarations.table.symbol(symbol);
entry.declarations.iter().all(|site| {
matches!(&site.syntax, SyntaxRef::Property(property)
if matches!(&property.body, FunctionBody::Accessors(accessors)
if accessors.accessors.iter().all(|accessor| matches!(
accessor.body,
FunctionBody::None { .. }
))))
})
self.declarations.table.symbol(symbol).is_auto_property()
}

fn collect_statics(&mut self, class: SymbolId, export: bool) {
Expand Down Expand Up @@ -4048,6 +4119,7 @@ impl<'a, 'ast> Generator<'a, 'ast> {
entries.push(EventEntry {
name: layout.event,
key,
before: Vec::new(),
arguments,
returns_value: false,
result_slot,
Expand Down Expand Up @@ -4418,8 +4490,8 @@ impl<'a, 'ast> Generator<'a, 'ast> {
member
}

/// The heap slot for a member declared directly on `MenSharpBehaviour`
/// (`gameObject`, `transform`), or `None` for anything else.
/// The heap slot for an auto-property declared directly on
/// `MenSharpBehaviour`, or `None` for anything else.
///
/// Udon has no `this` and no extern that returns a program's own object,
/// so these are not calls: each gets a private slot whose initial value is
Expand All @@ -4433,6 +4505,9 @@ impl<'a, 'ast> Generator<'a, 'ast> {
if symbol.parent != Some(marker) {
return None;
}
if !self.is_auto_property(member) {
return None;
}
if let Some(&slot) = self.statics.get(&member) {
return Some(slot);
}
Expand Down
4 changes: 3 additions & 1 deletion src/men-sharp-codegen/src/generator/exceptions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -412,7 +412,9 @@ impl<'a, 'ast> Generator<'a, 'ast> {
self.ensure_function(&key);
let function = &self.functions[&key];
let (label, return_slot) = (function.label, function.return_slot);
let halt = self.code_address_constant(format!("__halt_after_{name}"), None);
let halt =
self.code_address_constant(format!("__halt_after_{name}_{}", self.temp_counter), None);
self.temp_counter += 1;
self.copy(halt, return_slot);
self.program.code.push(Op::Jump(Target::Label(label)));
self.program.code.push(Op::Label(ok));
Expand Down
120 changes: 120 additions & 0 deletions src/men-sharp-compiler/tests/codegen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10822,6 +10822,126 @@ fn cancellation_stops_a_waiting_method_at_its_await() {
);
}

#[test]
fn destroy_cancellation_token_is_lazy_and_cancelled_before_the_user_handler() {
let mut sources = vec![SourceCode::new(
"base.cs",
r#"
namespace MenSharp
{
public class MenSharpBehaviour
{
private System.Threading.CancellationTokenSource destroyTokenSource;
public bool destroyTokenSourceCreated;
public System.Threading.CancellationToken destroyCancellationToken
{
get
{
if (destroyTokenSource == null)
{
destroyTokenSource = new System.Threading.CancellationTokenSource();
destroyTokenSourceCreated = true;
}
return destroyTokenSource.Token;
}
}
internal void __CancelDestroyToken()
{
if (destroyTokenSource != null)
destroyTokenSource.Cancel();
}
}
}
"#,
)];
sources.push(SourceCode::new(
"test.cs",
r#"
using MenSharp;
public class Probe : MenSharpBehaviour
{
public bool cancelled;
public bool observedBeforeDestroy;
public bool cached;
public void Start()
{
observedBeforeDestroy = destroyTokenSourceCreated;
cached = destroyCancellationToken.CanBeCanceled;
}
public void OnDestroy() { cancelled = destroyCancellationToken.IsCancellationRequested; }
}
"#,
));
sources.extend(
Compiler::corlib_sources()
.into_iter()
.filter(|source| source.name.as_ref() != "corlib/MenSharpBehaviour.cs"),
);
let Some(program) = compile_behaviour(sources, "Probe") else {
eprintln!("skipped: no .NET runtime");
return;
};
assert!(
program.output.errors.is_empty(),
"{:#?}",
program.output.errors
);
assert!(
!program
.output
.program
.data
.iter()
.any(|symbol| symbol.name == "__this_destroyCancellationToken")
);
// Unity's symbol table rejects duplicate heap names.
let mut data_names = std::collections::HashSet::new();
assert!(
program
.output
.program
.data
.iter()
.all(|symbol| data_names.insert(&symbol.name)),
"generated data symbols must have unique names"
);

let assembled = program.output.program.assemble().unwrap();
// An uncached token is created by OnDestroy itself, after the pre-hook.
let mut uncached = Emulator::new(&program.output.program, &assembled);
uncached.run(&assembled, "_onDestroy").unwrap();
assert!(matches!(
uncached.value_of("cancelled"),
Some(Value::Boolean(false))
));
assert!(matches!(
uncached.value_of("destroyTokenSourceCreated"),
Some(Value::Boolean(true))
));

// A cached token is canceled by the pre-hook before OnDestroy runs.
let mut emulator = Emulator::new(&program.output.program, &assembled);
// Start is a built-in Udon entry point; arbitrary public methods are not.
emulator.run(&assembled, "_start").unwrap();
assert!(matches!(
emulator.value_of("observedBeforeDestroy"),
Some(Value::Boolean(false))
));
assert!(matches!(
emulator.value_of("cached"),
Some(Value::Boolean(true))
));
emulator.run(&assembled, "_onDestroy").unwrap();
assert!(matches!(
emulator.value_of("cancelled"),
Some(Value::Boolean(true))
));
assert!(matches!(
emulator.value_of("destroyTokenSourceCreated"),
Some(Value::Boolean(true))
));
}

#[test]
fn iterator_misuse_is_rejected_by_the_checker() {
let Some(errors) = body_errors(
Expand Down
20 changes: 20 additions & 0 deletions src/men-sharp-semantics/src/symbol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,26 @@ impl<'ast> Symbol<'ast> {
self.member_map.get(name).map(Vec::as_slice).unwrap_or(&[])
}

/// Whether this source symbol declares an auto-property whose accessors
/// have no bodies, such as `{ get; }` or `{ get; set; }`.
pub fn is_auto_property(&self) -> bool {
self.kind == SymbolKind::Property
&& self.declarations.iter().all(|declaration| {
matches!(
&declaration.syntax,
SyntaxRef::Property(property)
if matches!(
&property.body,
FunctionBody::Accessors(accessors)
if accessors.accessors.iter().all(|accessor| matches!(
accessor.body,
FunctionBody::None { .. }
))
)
)
})
}

pub fn member_names(&self) -> impl Iterator<Item = (&'ast str, &[SymbolId])> {
self.member_map
.iter()
Expand Down
18 changes: 18 additions & 0 deletions tests/unity-project/Assets/MenSharp/MenSharpRuntimeSmoke.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,24 @@ public class MenSharpRuntimeSmoke : MenSharpBehaviour
public int syncResult;
public string syncText;

// OnDestroy cancels a token that was requested before destruction.
public bool cacheDestroyToken;
public bool destroyTokenCached;
public bool destroyTokenCanceled;

public void Start()
{
if (cacheDestroyToken)
{
destroyTokenCached = destroyCancellationToken.CanBeCanceled;
}
}

public void OnDestroy()
{
destroyTokenCanceled = destroyCancellationToken.IsCancellationRequested;
}

public bool asyncDone;
public int asyncResult;

Expand Down
Loading
Loading