Skip to content

Instantly share code, notes, and snippets.

@dresswithpockets
Last active April 17, 2022 08:27
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dresswithpockets/0c2f013e384ef58a2d4b7732840013e5 to your computer and use it in GitHub Desktop.
Save dresswithpockets/0c2f013e384ef58a2d4b7732840013e5 to your computer and use it in GitHub Desktop.
Odin gdnative binding borked on linux, works fine on windows.

This example is going off of the official docs for a GDNative library in C.

Folder structure:

dist/
  ...
lib/gdnative/
  gdnative.odin
src/
  test.odin
build.sh

You can find the gdnative-odin binding used for this test, here.

Build by running ./build.sh dll on windows, ./build.sh so on linux. It is built for x64 target with PIC enabled.

The example works completely fine on windows, it only works partially on linux.

When executing, the script will successfully finish calling godot_nativescript_init; but, when attempting to construct an instance of simple, the game crashes with no relevant output or error, even with --debug and --verbose flags enabled.

To be clear: [romper] nativescript init end is printed, but [romper] simple_constructor is never printed, and the crash occurs in between these two.

N.B. Some of these @(export)'s are unecessary, i know, i just threw them in for good measure ;) the behaviour is the same regardless.

odin build src/ -out:dist/romper.$1 -collection:lib=./lib -build-mode:dll
package romper
import "core:os"
import "core:runtime"
import "core:mem"
import gd "lib:gdnative"
gd_context :: proc "contextless" (gd_api: ^gd.GdnativeCoreApiStruct) -> runtime.Context {
c : runtime.Context
_gd_context_init(gd_api, &c)
return c
}
_gd_context_init :: proc "contextless" (gd_api: ^gd.GdnativeCoreApiStruct, c: ^runtime.Context) {
if c == nil {
return
}
c.allocator.procedure = gd_allocator_proc
c.allocator.data = gd_api
c.temp_allocator.procedure = runtime.default_temp_allocator_proc
c.temp_allocator.data = &runtime.global_default_temp_allocator_data
when !ODIN_DISABLE_ASSERT {
c.assertion_failure_proc = runtime.default_assertion_failure_proc
}
c.logger.procedure = runtime.default_logger_proc // todo(ash): godot print/err logger
c.logger.data = gd_api
}
gd_allocator :: proc "contextless" (gd_api: ^gd.GdnativeCoreApiStruct) -> mem.Allocator {
return mem.Allocator{
procedure = gd_allocator_proc,
data = gd_api,
}
}
gd_allocator_proc :: proc(allocator_data: rawptr,
mode: mem.Allocator_Mode,
size,
alignment: int,
old_memory: rawptr,
old_size: int,
loc := #caller_location) -> ([]byte, mem.Allocator_Error) {
gd_api := cast(^gd.GdnativeCoreApiStruct)allocator_data
switch mode {
case .Alloc:
ptr := gd_api.godot_alloc(cast(gd.Int)size)
if ptr == nil {
return nil, .Out_Of_Memory
}
return mem.byte_slice(ptr, size), nil
case .Free:
gd_api.godot_free(old_memory)
case .Free_All:
return nil, .Mode_Not_Implemented
case .Resize:
ptr: rawptr
if old_memory == nil {
ptr = gd_api.godot_alloc(cast(gd.Int)size)
if ptr == nil {
return nil, .Out_Of_Memory
}
return mem.byte_slice(ptr, size), nil
}
ptr = gd_api.godot_realloc(ptr, cast(gd.Int)size)
if ptr == nil {
return nil, .Out_Of_Memory
}
return mem.byte_slice(ptr, size), nil
case .Query_Features:
set := (^mem.Allocator_Mode_Set)(old_memory)
if set != nil {
set^ = {.Alloc, .Free, .Resize, .Query_Features}
}
return nil, nil
case .Query_Info:
return nil, .Mode_Not_Implemented
}
return nil, nil
}
package romper
import gd "lib:gdnative"
import "core:strings"
import "core:mem"
import "core:fmt"
import "core:runtime"
@(private)
gd_api: ^gd.GdnativeCoreApiStruct = nil
@(private)
ns_api: ^gd.GdnativeExtNativescriptApiStruct = nil
@(private)
global_ctx: runtime.Context
@(private)
global_method_data := "bwuh!"
@(export)
godot_gdnative_init :: proc "c" (options: ^gd.GdnativeInitOptions) {
gd_api = options.api_struct
global_ctx = gd_context(gd_api)
context = global_ctx
gd_print("[romper] init begin")
ext_search: for i in 0..<gd_api.num_extensions {
extension: ^gd.GdnativeApiStruct = mem.ptr_offset(gd_api.extensions, i)^
#partial switch extension.type {
case .GdnativeExtNativescript: {
gd_print("[romper] acquiring ns_api (ExtNativescript)")
ns_api = transmute(^gd.GdnativeExtNativescriptApiStruct)extension
break ext_search
}
}
}
gd_print("[romper] init end")
}
@(export)
godot_gdnative_terminate :: proc "c" (options: ^gd.GdnativeInitOptions) {
context = global_ctx
// gd_print("[romper] terminating")
gd_api = nil
ns_api = nil
}
@(export)
godot_nativescript_init :: proc "c" (handle: rawptr) {
context = global_ctx
gd_print("[romper] nativescript init begin (copying global method data pointer)")
create := gd.InstanceCreateFunc{nil,nil,nil}
create.create_func = simple_constructor
destroy := gd.InstanceDestroyFunc{nil,nil,nil}
destroy.destroy_func = simple_destructor
gd_print("[romper] nativescript registering romper_simple class")
ns_api.godot_nativescript_register_class(handle, "romper_simple", "Reference", create, destroy)
get_data := gd.InstanceMethod{}
get_data.method = simple_get_data
attributes := gd.MethodAttributes{.Disabled}
gd_print("[romper] nativescript registering romper_simple.get_data method handler")
ns_api.godot_nativescript_register_method(handle, "romper_simple", "get_data", attributes, get_data)
gd_print("[romper] nativescript init end")
}
UserData :: struct {
message: string,
to_free: [dynamic]rawptr,
}
simple_constructor :: proc "c" (instance, r_method_data: rawptr) -> rawptr {
context = global_ctx
gd_print("[romper] simple_constructor")
user_data := new(UserData)
user_data.message = "World from GDNative!"
user_data.to_free = make([dynamic]rawptr, 0, 1)
return user_data
}
simple_destructor :: proc "c" (instance, r_method_data, r_user_data: rawptr) {
context = global_ctx
gd_print("[romper] simple_destructor")
user_data := cast(^UserData)r_user_data
for ptr in user_data.to_free {
if ptr != nil {
free(ptr)
}
}
delete(user_data.to_free)
free(user_data)
}
simple_get_data :: proc "c" (instance, r_method_data, r_user_data: rawptr, num_args: gd.Int, args: [^]gd.Variant) -> gd.Variant {
context = global_ctx
gd_print("[romper] simple_get_data")
user_data := cast(^UserData)r_user_data
cstr := strings.clone_to_cstring(user_data.message)
defer free(cast(rawptr)cstr)
data: gd.String
ret: gd.Variant
gd_api.godot_string_new(&data)
gd_api.godot_string_parse_utf8(&data, cstr)
gd_api.godot_variant_new_string(&ret, &data)
gd_api.godot_string_destroy(&data)
return ret;
}
gd_print :: proc(msg: string) {
cstr := strings.clone_to_cstring(msg)
defer free(cast(rawptr)cstr)
data: gd.String
gd_api.godot_string_new(&data)
defer gd_api.godot_string_destroy(&data)
gd_api.godot_string_parse_utf8(&data, cstr)
gd_api.godot_print(&data)
}
gd_print_unsafe :: proc "contextless" (msg: string) {
d := transmute(mem.Raw_String)msg
cstr := cstring(d.data)
data: gd.String
gd_api.godot_string_new(&data)
defer gd_api.godot_string_destroy(&data)
gd_api.godot_string_parse_utf8(&data, cstr)
gd_api.godot_print(&data)
}
extends Button
const SIMPLE = preload("res://romper/simple.gdns")
var data
func _on_Button_pressed():
if data:
print("Data = " + data.get_data())
else:
data = SIMPLE.new()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment