Skip to content

Commit dffe189

Browse files
authored
Merge pull request #774 from lilizoey/refactor/godot-ffi-cleanup-unsafe
More thoroughly document `unsafe` in `godot-ffi`
2 parents 94ee30b + 5293ce8 commit dffe189

File tree

13 files changed

+270
-181
lines changed

13 files changed

+270
-181
lines changed

godot-codegen/src/generator/extension_interface.rs

+3
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,9 @@ fn generate_proc_address_funcs(h_path: &Path) -> TokenStream {
8484
}
8585

8686
impl GDExtensionInterface {
87+
// TODO: Figure out the right safety preconditions. This currently does not have any because incomplete safety docs
88+
// can cause issues with people assuming they are sufficient.
89+
#[allow(clippy::missing_safety_doc)]
8790
pub(crate) unsafe fn load(
8891
get_proc_address: crate::GDExtensionInterfaceGetProcAddress,
8992
) -> Self {

godot-codegen/src/generator/method_tables.rs

+12-3
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,10 @@ fn make_named_method_table(info: NamedMethodTable) -> TokenStream {
286286
pub const CLASS_COUNT: usize = #class_count;
287287
pub const METHOD_COUNT: usize = #method_count;
288288

289-
pub fn load(
289+
// TODO: Figure out the right safety preconditions. This currently does not have any because incomplete safety docs
290+
// can cause issues with people assuming they are sufficient.
291+
#[allow(clippy::missing_safety_doc)]
292+
pub unsafe fn load(
290293
#ctor_parameters
291294
) -> Self {
292295
#pre_init_code
@@ -383,8 +386,11 @@ fn make_method_table(info: IndexedMethodTable) -> TokenStream {
383386
pub const CLASS_COUNT: usize = #class_count;
384387
pub const METHOD_COUNT: usize = #method_count;
385388

389+
// TODO: Figure out the right safety preconditions. This currently does not have any because incomplete safety docs
390+
// can cause issues with people assuming they are sufficient.
391+
#[allow(clippy::missing_safety_doc)]
386392
#unused_attr
387-
pub fn load(
393+
pub unsafe fn load(
388394
#ctor_parameters
389395
) -> Self {
390396
#pre_init_code
@@ -455,8 +461,11 @@ fn make_method_table(info: IndexedMethodTable) -> TokenStream {
455461
pub const CLASS_COUNT: usize = #class_count;
456462
pub const METHOD_COUNT: usize = #method_count;
457463

464+
// TODO: Figure out the right safety preconditions. This currently does not have any because incomplete safety docs
465+
// can cause issues with people assuming they are sufficient.
466+
#[allow(clippy::missing_safety_doc)]
458467
#unused_attr
459-
pub fn load() -> Self {
468+
pub unsafe fn load() -> Self {
460469
// SAFETY: interface and lifecycle tables are initialized at this point, so we can get 'static references to them.
461470
let (interface, lifecycle_table) = unsafe {
462471
(crate::get_interface(), crate::builtin_lifecycle_api())

godot-core/src/init/mod.rs

+30-59
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,9 @@ unsafe extern "C" fn ffi_initialize_layer<E: ExtensionLibrary>(
9090
LEVEL_SERVERS_CORE_LOADED.store(true, Relaxed);
9191
}
9292

93-
gdext_on_level_init(level);
93+
// SAFETY: Godot will call this from the main thread, after `__gdext_load_library` where the library is initialized,
94+
// and only once per level.
95+
unsafe { gdext_on_level_init(level) };
9496
E::on_level_init(level);
9597
}
9698

@@ -120,27 +122,27 @@ unsafe extern "C" fn ffi_deinitialize_layer<E: ExtensionLibrary>(
120122
}
121123

122124
/// Tasks needed to be done by gdext internally upon loading an initialization level. Called before user code.
123-
fn gdext_on_level_init(level: InitLevel) {
124-
// SAFETY: we are in the main thread, during initialization, no other logic is happening.
125+
///
126+
/// # Safety
127+
///
128+
/// - Must be called from the main thread.
129+
/// - The interface must have been initialized.
130+
/// - Must only be called once per level.
131+
#[deny(unsafe_op_in_unsafe_fn)]
132+
unsafe fn gdext_on_level_init(level: InitLevel) {
125133
// TODO: in theory, a user could start a thread in one of the early levels, and run concurrent code that messes with the global state
126134
// (e.g. class registration). This would break the assumption that the load_class_method_table() calls are exclusive.
127135
// We could maybe protect globals with a mutex until initialization is complete, and then move it to a directly-accessible, read-only static.
128-
unsafe {
129-
match level {
130-
InitLevel::Core => {}
131-
InitLevel::Servers => {
132-
sys::load_class_method_table(sys::ClassApiLevel::Server);
133-
}
134-
InitLevel::Scene => {
135-
sys::load_class_method_table(sys::ClassApiLevel::Scene);
136-
ensure_godot_features_compatible();
137-
}
138-
InitLevel::Editor => {
139-
sys::load_class_method_table(sys::ClassApiLevel::Editor);
140-
}
141-
}
142-
crate::registry::class::auto_register_classes(level);
136+
137+
// SAFETY: we are in the main thread, initialize has been called, has never been called with this level before.
138+
unsafe { sys::load_class_method_table(level) };
139+
140+
if level == InitLevel::Scene {
141+
// SAFETY: On the main thread, api initialized, `Scene` was initialized above.
142+
unsafe { ensure_godot_features_compatible() };
143143
}
144+
145+
crate::registry::class::auto_register_classes(level);
144146
}
145147

146148
/// Tasks needed to be done by gdext internally upon unloading an initialization level. Called after user code.
@@ -271,49 +273,17 @@ pub enum EditorRunBehavior {
271273
/// See also:
272274
/// - [`ExtensionLibrary::on_level_init()`]
273275
/// - [`ExtensionLibrary::on_level_deinit()`]
274-
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
275-
pub enum InitLevel {
276-
/// First level loaded by Godot. Builtin types are available, classes are not.
277-
Core,
278-
279-
/// Second level loaded by Godot. Only server classes and builtins are available.
280-
Servers,
281-
282-
/// Third level loaded by Godot. Most classes are available.
283-
Scene,
284-
285-
/// Fourth level loaded by Godot, only in the editor. All classes are available.
286-
Editor,
287-
}
288-
289-
impl InitLevel {
290-
#[doc(hidden)]
291-
pub fn from_sys(level: godot_ffi::GDExtensionInitializationLevel) -> Self {
292-
match level {
293-
sys::GDEXTENSION_INITIALIZATION_CORE => Self::Core,
294-
sys::GDEXTENSION_INITIALIZATION_SERVERS => Self::Servers,
295-
sys::GDEXTENSION_INITIALIZATION_SCENE => Self::Scene,
296-
sys::GDEXTENSION_INITIALIZATION_EDITOR => Self::Editor,
297-
_ => {
298-
eprintln!("WARNING: unknown initialization level {level}");
299-
Self::Scene
300-
}
301-
}
302-
}
303-
#[doc(hidden)]
304-
pub fn to_sys(self) -> godot_ffi::GDExtensionInitializationLevel {
305-
match self {
306-
Self::Core => sys::GDEXTENSION_INITIALIZATION_CORE,
307-
Self::Servers => sys::GDEXTENSION_INITIALIZATION_SERVERS,
308-
Self::Scene => sys::GDEXTENSION_INITIALIZATION_SCENE,
309-
Self::Editor => sys::GDEXTENSION_INITIALIZATION_EDITOR,
310-
}
311-
}
312-
}
276+
pub type InitLevel = sys::InitLevel;
313277

314278
// ----------------------------------------------------------------------------------------------------------------------------------------------
315279

316-
fn ensure_godot_features_compatible() {
280+
/// # Safety
281+
///
282+
/// - Must be called from the main thread.
283+
/// - The interface must be initialized.
284+
/// - The `Scene` api level must have been initialized.
285+
#[deny(unsafe_op_in_unsafe_fn)]
286+
unsafe fn ensure_godot_features_compatible() {
317287
// The reason why we don't simply call Os::has_feature() here is that we might move the high-level engine classes out of godot-core
318288
// later, and godot-core would only depend on godot-sys. This makes future migrations easier. We still have access to builtins though.
319289

@@ -323,8 +293,9 @@ fn ensure_godot_features_compatible() {
323293
let single = GString::from("single");
324294
let double = GString::from("double");
325295

326-
// SAFETY: main thread, after initialize(), valid string pointers.
327296
let gdext_is_double = cfg!(feature = "double-precision");
297+
298+
// SAFETY: main thread, after initialize(), valid string pointers, `Scene` initialized.
328299
let godot_is_double = unsafe {
329300
let is_single = sys::godot_has_feature(os_class.string_sys(), single.sys());
330301
let is_double = sys::godot_has_feature(os_class.string_sys(), double.sys());

godot-ffi/src/binding/mod.rs

+21-19
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,27 @@ unsafe impl Sync for ClassLibraryPtr {}
7979
// SAFETY: See `Sync` impl safety doc.
8080
unsafe impl Send for ClassLibraryPtr {}
8181

82+
// ----------------------------------------------------------------------------------------------------------------------------------------------
83+
84+
/// # Safety
85+
/// The table must not have been initialized yet.
86+
unsafe fn initialize_table<T>(table: &ManualInitCell<T>, value: T, what: &str) {
87+
debug_assert!(
88+
!table.is_initialized(),
89+
"method table for {what} should only be initialized once"
90+
);
91+
92+
table.set(value)
93+
}
94+
95+
/// # Safety
96+
/// The table must have been initialized.
97+
unsafe fn get_table<T>(table: &'static ManualInitCell<T>, msg: &str) -> &'static T {
98+
debug_assert!(table.is_initialized(), "{msg}");
99+
100+
table.get_unchecked()
101+
}
102+
82103
// ----------------------------------------------------------------------------------------------------------------------------------------------
83104
// Public API
84105

@@ -154,25 +175,6 @@ pub unsafe fn class_editor_api() -> &'static ClassEditorMethodTable {
154175
)
155176
}
156177

157-
/// # Safety
158-
/// The table must not have been initialized yet.
159-
unsafe fn initialize_table<T>(table: &ManualInitCell<T>, value: T, what: &str) {
160-
debug_assert!(
161-
!table.is_initialized(),
162-
"method table for {what} should only be initialized once"
163-
);
164-
165-
table.set(value)
166-
}
167-
168-
/// # Safety
169-
/// The table must have been initialized.
170-
unsafe fn get_table<T>(table: &'static ManualInitCell<T>, msg: &str) -> &'static T {
171-
debug_assert!(table.is_initialized(), "{msg}");
172-
173-
table.get_unchecked()
174-
}
175-
176178
/// # Safety
177179
///
178180
/// - The Godot binding must have been initialized before calling this function.

godot-ffi/src/binding/single_threaded.rs

+3-1
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,9 @@ impl BindingStorage {
143143
/// - The binding must be initialized.
144144
#[inline(always)]
145145
pub unsafe fn get_binding_unchecked() -> &'static GodotBinding {
146-
let storage = Self::storage();
146+
// SAFETY: The bindings were initialized on the main thread because `initialize` must be called from the main thread,
147+
// and this function is called from the main thread.
148+
let storage = unsafe { Self::storage() };
147149

148150
// We only check if we are in the main thread in debug builds if we aren't building for a non-threaded Godot build,
149151
// since we could otherwise assume there won't be multi-threading.

godot-ffi/src/compat/compat_4_0.rs

+4-3
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ use crate::compat::BindingCompat;
1515

1616
pub type InitCompat = *const sys::GDExtensionInterface;
1717

18-
impl BindingCompat for *const sys::GDExtensionInterface {
18+
// SAFETY: If `ensure_static_runtime_compatibility` succeeds then the other two functions should be safe to call.
19+
unsafe impl BindingCompat for *const sys::GDExtensionInterface {
1920
fn ensure_static_runtime_compatibility(&self) {
2021
// We try to read the first fields of the GDExtensionInterface struct, which are version numbers.
2122
// If those are unrealistic numbers, chances are high that `self` is in fact a function pointer (used for Godot 4.1.x).
@@ -40,7 +41,7 @@ impl BindingCompat for *const sys::GDExtensionInterface {
4041
);
4142
}
4243

43-
fn runtime_version(&self) -> sys::GDExtensionGodotVersion {
44+
unsafe fn runtime_version(&self) -> sys::GDExtensionGodotVersion {
4445
// SAFETY: this method is only invoked after the static compatibility check has passed.
4546
// We thus know that Godot 4.0.x runs, and *self is a GDExtensionInterface pointer.
4647
let interface = unsafe { &**self };
@@ -52,7 +53,7 @@ impl BindingCompat for *const sys::GDExtensionInterface {
5253
}
5354
}
5455

55-
fn load_interface(&self) -> sys::GDExtensionInterface {
56+
unsafe fn load_interface(&self) -> sys::GDExtensionInterface {
5657
unsafe { **self }
5758
}
5859
}

godot-ffi/src/compat/compat_4_1plus.rs

+25-13
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@ use crate::compat::BindingCompat;
1717

1818
pub type InitCompat = sys::GDExtensionInterfaceGetProcAddress;
1919

20-
impl BindingCompat for sys::GDExtensionInterfaceGetProcAddress {
20+
// SAFETY: If `ensure_static_runtime_compatibility` succeeds then the other two functions should be safe to call, except on wasm where
21+
// `ensure_static_runtime_compatibility` is a no-op.
22+
// TODO: fix `ensure_static_runtime_compatibility` on wasm.
23+
unsafe impl BindingCompat for sys::GDExtensionInterfaceGetProcAddress {
2124
// In WebAssembly, function references and data pointers live in different memory spaces, so trying to read the "memory"
2225
// at a function pointer (an index into a table) to heuristically determine which API we have (as is done below) won't work.
2326
#[cfg(target_family = "wasm")]
@@ -82,7 +85,9 @@ impl BindingCompat for sys::GDExtensionInterfaceGetProcAddress {
8285
// From here we can assume Godot 4.1+. We need to make sure that the runtime version is >= static version.
8386
// Lexicographical tuple comparison does that.
8487
let static_version = crate::GdextBuild::godot_static_version_triple();
85-
let runtime_version_raw = self.runtime_version();
88+
89+
// SAFETY: We are now reasonably sure the runtime version is 4.1.
90+
let runtime_version_raw = unsafe { self.runtime_version() };
8691

8792
// SAFETY: Godot provides this version struct.
8893
let runtime_version = (
@@ -105,21 +110,28 @@ impl BindingCompat for sys::GDExtensionInterfaceGetProcAddress {
105110
}
106111
}
107112

108-
fn runtime_version(&self) -> sys::GDExtensionGodotVersion {
109-
unsafe {
110-
let get_proc_address = self.expect("get_proc_address unexpectedly null");
111-
let get_godot_version = get_proc_address(sys::c_str(b"get_godot_version\0")); //.expect("get_godot_version unexpectedly null");
113+
unsafe fn runtime_version(&self) -> sys::GDExtensionGodotVersion {
114+
let get_proc_address = self.expect("get_proc_address unexpectedly null");
112115

113-
let get_godot_version =
114-
crate::cast_fn_ptr!(get_godot_version as sys::GDExtensionInterfaceGetGodotVersion);
116+
// SAFETY: `self.0` is a valid `get_proc_address` pointer.
117+
let get_godot_version = unsafe { get_proc_address(sys::c_str(b"get_godot_version\0")) }; //.expect("get_godot_version unexpectedly null");
115118

116-
let mut version = std::mem::MaybeUninit::<sys::GDExtensionGodotVersion>::zeroed();
117-
get_godot_version(version.as_mut_ptr());
118-
version.assume_init()
119-
}
119+
// SAFETY: `sys::GDExtensionInterfaceGetGodotVersion` is an `Option` of an `unsafe extern "C"` function pointer.
120+
let get_godot_version = crate::cast_fn_ptr!(unsafe {
121+
get_godot_version as sys::GDExtensionInterfaceGetGodotVersion
122+
});
123+
124+
let mut version = std::mem::MaybeUninit::<sys::GDExtensionGodotVersion>::zeroed();
125+
126+
// SAFETY: `get_proc_address` with "get_godot_version" does return a valid `sys::GDExtensionInterfaceGetGodotVersion` pointer, and since we have a valid
127+
// `get_proc_address` pointer then it must be callable.
128+
unsafe { get_godot_version(version.as_mut_ptr()) };
129+
130+
// SAFETY: `get_godot_version` initializes `version`.
131+
unsafe { version.assume_init() }
120132
}
121133

122-
fn load_interface(&self) -> sys::GDExtensionInterface {
134+
unsafe fn load_interface(&self) -> sys::GDExtensionInterface {
123135
unsafe { sys::GDExtensionInterface::load(*self) }
124136
}
125137
}

godot-ffi/src/compat/mod.rs

+16-3
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,12 @@ pub use compat_4_0::*;
2121
///
2222
/// Provides a compatibility layer to be able to use 4.0.x extensions under Godot versions >= 4.1.
2323
/// Also performs deterministic checks and expressive errors for cases where compatibility cannot be provided.
24-
pub(crate) trait BindingCompat {
24+
///
25+
/// # Safety
26+
///
27+
/// [`ensure_static_runtime_compatibility`](BindingCompat::ensure_static_runtime_compatibility) succeeding should be sufficient to ensure that
28+
/// both [`runtime_version`](BindingCompat::runtime_version) and [`load_interface`](BindingCompat::load_interface) can be called safely.
29+
pub(crate) unsafe trait BindingCompat {
2530
// Implementation note: these methods could be unsafe, but that would remove any `unsafe` statements _inside_
2631
// the function bodies, making reasoning about them harder. Also, the call site is already an unsafe function,
2732
// so it would not add safety there, either.
@@ -42,8 +47,16 @@ pub(crate) trait BindingCompat {
4247
fn ensure_static_runtime_compatibility(&self);
4348

4449
/// Return version dynamically passed via `gdextension_interface.h` file.
45-
fn runtime_version(&self) -> sys::GDExtensionGodotVersion;
50+
///
51+
/// # Safety
52+
///
53+
/// `self` must be a valid interface or get proc address pointer.
54+
unsafe fn runtime_version(&self) -> sys::GDExtensionGodotVersion;
4655

4756
/// Return the interface, either as-is from the header (legacy) or code-generated (modern API).
48-
fn load_interface(&self) -> sys::GDExtensionInterface;
57+
///
58+
/// # Safety
59+
///
60+
/// `self` must be a valid interface or get proc address pointer.
61+
unsafe fn load_interface(&self) -> sys::GDExtensionInterface;
4962
}

0 commit comments

Comments
 (0)