Skip to content

Raygui Bindings#10

Open
BrownNPC wants to merge 6 commits intomasterfrom
raygui
Open

Raygui Bindings#10
BrownNPC wants to merge 6 commits intomasterfrom
raygui

Conversation

@BrownNPC
Copy link
Copy Markdown
Owner

@BrownNPC BrownNPC commented Apr 21, 2026

To develop these bindings, fork the repo. Do git checkout raygui.

the raygui bindings are in the raygui directory.

to view the changes while developing you can start a web server in the index folder eg.
cd index && python -m http.server

And then you can edit the example in examples/raygui and build it into the index folder. eg.

cd examples/raygui

GOOS=js GOARCH=wasm go build -o ../../index/main.wasm .

Open the http server url in your browser. Make sure to spam shift + f5 to refresh the page. Shift + f5 forces a cache refresh.

Summary by CodeRabbit

  • New Features

    • Added comprehensive GUI components and styling support and a new GUI example.
    • Enhanced WebAssembly runtime helpers for improved browser interop.
  • Refactor

    • Simplified startup flow — app now initializes immediately (loading overlay removed) and canvas context menu disabled.
  • Chores

    • Bumped Go toolchain to 1.26.1.

@BrownNPC
Copy link
Copy Markdown
Owner Author

#10

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 21, 2026

📝 Walkthrough

Walkthrough

Adds a WebAssembly runtime and codegen for Go raylib bindings, new JS runtime helpers for Go↔WASM interop, autogenerated raylib JS-target bindings and templates, RayGUI package and example, and refactors the web bootstrap (HTML/JS) for WASM startup.

Changes

Cohort / File(s) Summary
WASM runtime core & API metadata
wasm-runtime/wasm.go, wasm-runtime/api/rlapi.go, wasm-runtime/api/stringyvalue.go, wasm-runtime/go.mod
New JS-target wasm runtime helpers: C heap allocation/free imports, copy-to/from-C primitives, C string helpers, Go↔C conversion utilities, plus embedded rl API JSON unmarshalling types and flexible JSON string parsing.
Autogen templates & generator
wasm-runtime/templates/templates.go, wasm-runtime/templates/*.gotmpl
New template utilities and templates to generate rl package code (structs, enums, defines, aliases, func imports) and functions to render/format generated Go source.
Generated rl bindings (js build)
wasm-runtime/rl/structs_gen_formatted.go, wasm-runtime/rl/enums_gen_formatted.go, wasm-runtime/rl/defines_gen_formatted.go, wasm-runtime/rl/aliases_gen_formatted.go, wasm-runtime/rl/go.mod
Autogenerated rl package: many exported structs, enum/type aliases, constants, colors, and compatibility aliases for the raylib API targeting js builds.
RayGUI package & example module
raygui/raygui.go, raygui/go.mod, examples/raygui/main.go, examples/go.mod
New raygui package (JS build) exposing GUI types, constants, exported wrappers and wasm-imported GUI functions; example main.go (//go:build js) rendering a 500×500 window; updated example module manifests.
Web bootstrap & runtime JS
index/index.js, index/runtime.js, index/index.html
Refactored page (removed loading overlay), injects raylib runtime into go.importObject, assigns globalThis.raylib/mod/goInstance, introduces Runtime JS class with memory access and string/memory helpers, and changes WASM instantiation/run wiring.
Misc / cleanup
raylib/rcore_wasm.go, wasm-runtime/api/rlapi.go (also listed above)
Whitespace cleanup in rcore_wasm.go; rlapi.go adds exported Api and JSON-decoding on init.

Sequence Diagram(s)

sequenceDiagram
    participant Browser
    participant IndexJS as index.js
    participant RuntimeJS as runtime.js
    participant GoWasm as Go WASM Module
    participant RaylibHeap as raylib (C heap)

    Browser->>IndexJS: load ./index.js
    IndexJS->>RuntimeJS: new Runtime()
    IndexJS->>IndexJS: create raylib instance (Module factory)
    IndexJS->>GoWasm: inject into go.importObject { raylib, globalThis, gojs: Runtime helpers }
    IndexJS->>GoWasm: WebAssembly.instantiateStreaming(...) -> instance
    IndexJS->>IndexJS: globalThis.goInstance = instance
    IndexJS->>GoWasm: go.run(instance)
    GoWasm->>GoWasm: Go main() calls SetMain(update)
    GoWasm->>IndexJS: SetMain schedules requestAnimationFrame via imported JS
    loop every frame
        IndexJS->>GoWasm: animation frame -> call update()
        GoWasm->>RaylibHeap: request C allocations / copy data via runtime helpers
        GoWasm->>RaylibHeap: draw and end frame
        IndexJS->>IndexJS: requestAnimationFrame(next)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Poem

🐰 A hop, a thunk, a memory dance,

Go meets WASM, given a chance.
Runtime bridges byte and string,
RayGUI wakes and icons sing.
Tiny rabbit, big web prance.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Raygui Bindings' is directly related to the main change in this changeset, which adds comprehensive raygui bindings in the raygui directory with supporting infrastructure.
Docstring Coverage ✅ Passed Docstring coverage is 92.20% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch raygui

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.11.4)

level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies"


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@BrownNPC BrownNPC mentioned this pull request Apr 21, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🧹 Nitpick comments (10)
wasm-runtime/templates/templates.go (2)

29-57: Consider de-duplicating GenerateCode and GenerateCodeFormatted.

The two functions share identical parse + execute + write logic; only the post-processing (format.Source) and output suffix differ. Extracting a helper would reduce drift and the risk of the two paths diverging later. Also, GenerateCodeFormatted panics on format.Source errors — if a generated template is ever momentarily malformed, you lose the unformatted output that would help debug it. Writing the unformatted version on format failure (or including it in the panic message) makes diagnosis much easier.

♻️ Sketch
 func GenerateCodeFormatted(model any, templ string, name string, funcs template.FuncMap) {
     structs := LoadTemplateFuncs(templ, name, funcs)
     var buf bytes.Buffer
     if err := structs.Execute(&buf, model); err != nil {
         panic(err)
     }
     formatted, err := format.Source(buf.Bytes())
     if err != nil {
-        panic(err)
+        _ = os.WriteFile(fmt.Sprintf("rl/%s_gen_unformatted.go", name), buf.Bytes(), 0644)
+        panic(fmt.Errorf("format %s: %w (unformatted output written for inspection)", name, err))
     }
     ...
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wasm-runtime/templates/templates.go` around lines 29 - 57, Extract the shared
template load+execute+write logic from GenerateCode and GenerateCodeFormatted
into a helper (e.g., renderAndWriteTemplate or writeTemplateOutput) that takes
model, templ, name, funcs and a postProcess function (nil for no-op or a
formatter wrapper that returns formatted bytes or an error); call
LoadTemplateFuncs and Execute once in that helper and use it from both
GenerateCode and GenerateCodeFormatted; in the formatted path, if format.Source
returns an error, ensure the helper writes the unformatted buffer to
rl/{name}_gen_unformatted.go (or include the unformatted bytes in the
panic/error message) before returning or panicking so the raw output is
preserved for debugging.

19-25: Copy-pasted doc comment on LoadTemplateFuncs.

The doc comment still says "LoadTemplate" and doesn't mention that this variant registers a FuncMap. Same issue on line 40 for GenerateCodeFormatted (doc says "GenerateCode" and references the wrong output path — though the _gen_formatted.go suffix does actually differ, so at least the path is correct; only the function name/description is stale).

📝 Suggested doc fix
-// LoadTemplate takes in a gotempl as a string and a name for the template.
-// It panics if template could not be parsed.
+// LoadTemplateFuncs takes in a gotempl as a string, a name for the template,
+// and a FuncMap to register before parsing. It panics if the template could
+// not be parsed.
 func LoadTemplateFuncs(templ, name string, funcs template.FuncMap) *template.Template {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wasm-runtime/templates/templates.go` around lines 19 - 25, Update the stale
doc comment for LoadTemplateFuncs to refer to its actual name and behavior
(mention it registers the provided template.FuncMap and panics if parsing fails)
and likewise update the doc comment for GenerateCodeFormatted to reference
GenerateCodeFormatted (not GenerateCode) and describe that it writes formatted
output to the *_gen_formatted.go path; locate the comments above the
LoadTemplateFuncs and GenerateCodeFormatted function declarations and replace
the copied text with concise descriptions that include the function names and
their specific behaviors (registration of FuncMap for LoadTemplateFuncs and
formatted output path for GenerateCodeFormatted).
examples/raygui/main.go (1)

10-19: Missing defer rl.CloseWindow() and unchecked window init.

Minor lifecycle hygiene: after rl.InitWindow it's conventional to defer rl.CloseWindow() so GL/canvas resources get released when the loop exits. Since rl.SetMain blocks indefinitely on WASM (per raylib/raylib_wasm.go snippet 4), this is mostly cosmetic, but it's the idiomatic raylib-go pattern and keeps examples copy-paste-safe for non-WASM targets.

 func main() {
     rl.InitWindow(500, 500, "RayGUI Example")
+    defer rl.CloseWindow()
 
     update := func() {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/raygui/main.go` around lines 10 - 19, The main function calls
rl.InitWindow but never defers rl.CloseWindow and doesn't check InitWindow
success; update the lifecycle to call rl.InitWindow(500, 500, "RayGUI Example"),
immediately follow it with defer rl.CloseWindow() and (optionally) confirm
initialization succeeded before proceeding, then keep the existing update
closure and rl.SetMain usage (referencing functions InitWindow, CloseWindow,
update closure, and SetMain) so the window/GPU resources are released properly
on exit and the example is safe to copy to non-WASM targets.
wasm-runtime/templates/enums.go.gotmpl (1)

14-23: Empty descriptions produce bare // comment lines.

When .Description (on the enum or on a value) is empty, the template still emits // lines, producing stray/empty comments in the generated source. Guard with {{- if .Description}} as is done in defines.go.gotmpl. Same pattern applies to aliases.go.gotmpl (line 14) and funcimport.go.gotmpl (line 14) — see the separate concern on funcimport about the missing // prefix.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wasm-runtime/templates/enums.go.gotmpl` around lines 14 - 23, The template
emits empty comment lines when .Description is blank; update the enums template
to wrap the generated comment blocks with presence checks: use {{- if
.Description}}// {{ .Description}}{{- end}} for the enum description (iterating
.Enums) and similarly wrap the value comments inside the .Values loop so no bare
"//" is emitted; apply the same guard pattern to the aliases template (guard its
.Description) and to funcimport ensure you both guard the description and
include the missing "// " prefix before the description when present.
wasm-runtime/rl/go.mod (1)

1-3: Bare module rl and nested-module layout.

Declaring module rl (no import path prefix) means this module cannot be imported by any other module via its canonical path. It's also nested inside wasm-runtime/ which already has its own go.mod, so the two are separate modules and wasm-runtime/templates/templates.go writing into rl/ from the parent module won't see this as an imported package at compile time. If that's intentional (codegen-only, locally consumed via replace), consider at minimum using a qualified path like github.com/BrownNPC/Raylib-Go-Wasm/wasm-runtime/rl to avoid surprises when someone pulls it into another module, and document the nested-module setup in the PR description / README.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wasm-runtime/rl/go.mod` around lines 1 - 3, The go.mod in the nested module
currently declares an unqualified module name "module rl", which prevents other
modules from importing it and breaks the parent module's codegen expectations
(templates.go writing into rl/); change the module declaration in
wasm-runtime/rl/go.mod to a fully qualified import path (for example
github.com/YourOrg/YourRepo/wasm-runtime/rl) so it can be imported by canonical
path, and add a short note in the repo README or the PR description explaining
the nested-module layout and whether consumers should use a replace directive
for local development.
wasm-runtime/templates/aliases.go.gotmpl (1)

1-1: Unused $root assignment.

$root is assigned but never referenced inside this template. Same in enums.go.gotmpl, funcimport.go.gotmpl, and defines.go.gotmpl. Either drop it or use it where cross-scope access is needed inside range blocks.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wasm-runtime/templates/aliases.go.gotmpl` at line 1, The template sets an
unused variable "$root" ({{- $root := . -}}) in aliases.go.gotmpl,
enums.go.gotmpl, funcimport.go.gotmpl and defines.go.gotmpl; either remove that
assignment from each template to eliminate the dead binding or actually
reference "$root" where cross-scope access is required (e.g., inside a {{range}}
or {{with}} block) so the root context is used instead of "."; update the
templates accordingly by deleting the "$root" line when not needed or replacing
local uses of "." inside nested blocks with "$root" to access the top-level
context.
index/runtime.js (1)

41-58: Remove or finish the prototype imports before wiring them.

array logs raw stack details on every call, and struct still has “messing around” implementation/comments. If these imports are unused, drop them from the JS import object; if they are needed, make the behavior explicit and quiet.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@index/runtime.js` around lines 41 - 58, The exported prototype functions
array and struct are placeholders that either need to be removed from the JS
import object or fully implemented and silenced: delete the array and struct
entries from the import table if unused, otherwise replace the console.log spam
in array (function array = (sp) => { ... }) with deterministic behavior (e.g.,
compute and return slice length/capacity using this.getInt64(sp + 8 + 8) without
logging) and finish struct (function struct = (sp) => { ... }) by implementing
the intended reads/writes using this.getInt64 and this.setInt64/this.setInt32
(remove the "messing around" comments and commented-out lines), and ensure any
pointer arithmetic (sp + 8, sp + 16) is correctly unsigned (sp >>>= 0) and has
clear return values or side effects so the import is meaningful and quiet.
raygui/raygui.go (2)

21-27: Use keyed struct literals for rl.Color and prefer | when packing.

rl.Color{uint8(v >> 24), ...} relies on positional ordering of Color's fields (R,G,B,A). If the generated rl.Color ever changes field order, this will silently miscompile colors. A keyed literal documents intent and is robust to reordering. Using bitwise | for packing is also more idiomatic than + and safer if widths ever change.

♻️ Suggested refactor
 func (v PropertyValue) AsColor() rl.Color {
-	return rl.Color{uint8(v >> 24), uint8(v >> 16), uint8(v >> 8), uint8(v)}
+	return rl.Color{
+		R: uint8(v >> 24),
+		G: uint8(v >> 16),
+		B: uint8(v >> 8),
+		A: uint8(v),
+	}
 }

 func NewColorPropertyValue(color rl.Color) PropertyValue {
-	return PropertyValue(uint32(color.R)<<24 + uint32(color.G)<<16 + uint32(color.B)<<8 + uint32(color.A))
+	return PropertyValue(uint32(color.R)<<24 | uint32(color.G)<<16 | uint32(color.B)<<8 | uint32(color.A))
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@raygui/raygui.go` around lines 21 - 27, PropertyValue.AsColor currently
constructs rl.Color positionally and NewColorPropertyValue packs channels with
+; update AsColor to use a keyed struct literal (rl.Color{R: uint8(v >> 24), G:
uint8(v >> 16), B: uint8(v >> 8), A: uint8(v)}) and change NewColorPropertyValue
to use bitwise OR for packing (e.g. PropertyValue(uint32(color.R)<<24 |
uint32(color.G)<<16 | uint32(color.B)<<8 | uint32(color.A))) so the intent is
explicit and safe against field reordering or width issues; apply these changes
in the PropertyValue.AsColor and NewColorPropertyValue functions.

17-19: Extract the 16 threshold into a named constant.

p >= 16 duplicates the same magic number used throughout the file (PropertyID = 16 + iota, etc.) for the extended-property base. A named constant (e.g. MaxBaseProperties PropertyID = 16) used both here and in the extended-property const blocks would make the contract explicit and keep them in sync if raygui ever bumps RAYGUI_MAX_PROPS_BASE.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@raygui/raygui.go` around lines 17 - 19, Extract the magic number 16 into a
named constant and use it wherever the extended-property base is referenced:
define something like MaxBaseProperties PropertyID = 16 and replace the literal
in the PropertyID.IsExtended method (currently p >= 16) and in the PropertyID
const block(s) that use `PropertyID = 16 + iota` so both the check in IsExtended
and the extended-property base definitions reference the same constant.
wasm-runtime/rl/defines_gen_formatted.go (1)

21-97: Inconsistent casing across color variable names.

The generated color vars mix two styles: Darkgray, Darkblue, Darkpurple, Darkbrown, Skyblue, Raywhite use an all-lowercase suffix, while DarkGreen uses CamelCase. Since this file is autogenerated, the generator template is the right place to fix it — but it's worth flagging now because the names are effectively public API and renaming later would be a breaking change for downstream users.

Recommended: pick one convention (idiomatic Go would be DarkGray, DarkBlue, DarkPurple, DarkBrown, SkyBlue, RayWhite) and apply consistently.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wasm-runtime/rl/defines_gen_formatted.go` around lines 21 - 97, The generated
color variable names mix casing (e.g. Darkgray, Darkblue, Darkpurple, Darkbrown,
Skyblue, Raywhite vs DarkGreen), so update the generator template that emits
these vars to consistently use CamelCase for compound names (make
Darkgray→DarkGray, Darkblue→DarkBlue, Darkpurple→DarkPurple,
Darkbrown→DarkBrown, Skyblue→SkyBlue, Raywhite→RayWhite) so all exported Color
vars follow the same idiomatic Go casing (like DarkGreen); change the naming
logic in the generator where it formats identifiers for variables such as
Lightgray, Darkgreen, Skyblue, Raywhite to produce the corrected CamelCase
forms.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@examples/go.mod`:
- Around line 5-17: The replaces in examples/go.mod point at ../raylib and
../raygui but those local modules declare github.com/BrownNPC/Raylib-Go-Wasm/*
paths, so update the replace directives to target
github.com/BrownNPC/Raylib-Go-Wasm/raylib and
github.com/BrownNPC/Raylib-Go-Wasm/raygui (instead of github.com/gen2brain/...),
and also add a local replace for the wasm-runtime module used by raygui (the
wasm-runtime module path referenced inside the raygui module) to point at the
local directory (e.g., ../wasm-runtime) so the examples build uses the local
pseudo-modules rather than fetching remote pseudo-versions.

In `@examples/raygui/main.go`:
- Around line 5-8: The example imports the upstream gen2brain packages but calls
rl.SetMain (defined only in this fork); update the import statements in main.go
to use the forked module (replace github.com/gen2brain/raylib-go/raygui and
github.com/gen2brain/raylib-go/raylib with the BrownNPC/Raylib-Go-Wasm
equivalents) so rl.SetMain and the raygui bindings resolve correctly; locate the
import block and references to rl.SetMain/ raygui in the file (main.go) and
change the package paths to the forked repository.

In `@index/index.js`:
- Around line 41-47: The promise from
WebAssembly.instantiateStreaming(fetch("main.wasm"), go.importObject) lacks a
rejection handler; update the chain where WebAssembly.instantiateStreaming is
called so it handles errors (e.g., add a .catch) and logs the error with context
before failing gracefully; ensure the handler references the same scope that
sets globalThis.goInstance and calls go.run so failures to
fetch/compile/instantiate are logged (include identifying symbols:
WebAssembly.instantiateStreaming, fetch("main.wasm"), globalThis.goInstance,
go.run).

In `@index/runtime.js`:
- Around line 69-77: The CStringGetLength implementation can loop forever when
cStr is invalid or a NUL is missing; fix it by bounding the scan to the HEAPU8
buffer length and validating cStr before accessing memory: ensure cStr is a
non-negative integer less than raylib.HEAPU8.length, then iterate while index <
raylib.HEAPU8.length and view[index] !== 0, incrementing len; if cStr is out of
range treat length as 0 (or clamp to available bytes) and then call
this.setInt32(sp + 8 * 2, len). Also reference the variables CStringGetLength,
sp, cStr, raylib.HEAPU8, view and this.setInt32 when making the change.

In `@raygui/raygui.go`:
- Around line 186-189: The two constants collide because SPINNER_BUTTON_WIDTH is
set to the literal 16 so the following untyped SPINNER_BUTTON_SPACING repeats
16; change the const block to use iota like other blocks by making
SPINNER_BUTTON_WIDTH of type PropertyID equal to 16 + iota (e.g.,
"SPINNER_BUTTON_WIDTH PropertyID = 16 + iota") and leave SPINNER_BUTTON_SPACING
untyped so it becomes 17; update the const block containing SPINNER_BUTTON_WIDTH
and SPINNER_BUTTON_SPACING accordingly.

In `@wasm-runtime/go.mod`:
- Line 3: Two go.mod files declare different go directives ("go 1.26.1" vs "go
1.25.6"), so either make them consistent or document the deliberate mismatch;
pick the project-wide toolchain version you want and update the go directive in
the other module to match the chosen value (change the "go 1.26.1" or "go
1.25.6" entry accordingly), then run go mod tidy and verify local/CI builds; if
you intentionally need different versions, add a short note to the repo README
explaining why the go directives differ and which module requires which
toolchain.

In `@wasm-runtime/wasm.go`:
- Around line 98-128: copySliceToC and copySliceToGo panic on empty slices
because they access dst[0] without checking length and copySliceToGo writes into
capacity-only space; update both to handle zero-length slices: in copySliceToC
(function name copySliceToC) detect if len(s)==0 and return a nil Cptr (zero
value) with a no-op cleanup function (avoid malloc and _copyToC), and in
copySliceToGo (function name copySliceToGo) if len(dst)==0 simply return without
copying (do not fall back to cap(dst) or write into the underlying array),
keeping copyValueToGo unchanged; ensure any callers can accept a nil Cptr result
from copySliceToC or skip calling copySliceToGo when destination length is zero.

---

Nitpick comments:
In `@examples/raygui/main.go`:
- Around line 10-19: The main function calls rl.InitWindow but never defers
rl.CloseWindow and doesn't check InitWindow success; update the lifecycle to
call rl.InitWindow(500, 500, "RayGUI Example"), immediately follow it with defer
rl.CloseWindow() and (optionally) confirm initialization succeeded before
proceeding, then keep the existing update closure and rl.SetMain usage
(referencing functions InitWindow, CloseWindow, update closure, and SetMain) so
the window/GPU resources are released properly on exit and the example is safe
to copy to non-WASM targets.

In `@index/runtime.js`:
- Around line 41-58: The exported prototype functions array and struct are
placeholders that either need to be removed from the JS import object or fully
implemented and silenced: delete the array and struct entries from the import
table if unused, otherwise replace the console.log spam in array (function array
= (sp) => { ... }) with deterministic behavior (e.g., compute and return slice
length/capacity using this.getInt64(sp + 8 + 8) without logging) and finish
struct (function struct = (sp) => { ... }) by implementing the intended
reads/writes using this.getInt64 and this.setInt64/this.setInt32 (remove the
"messing around" comments and commented-out lines), and ensure any pointer
arithmetic (sp + 8, sp + 16) is correctly unsigned (sp >>>= 0) and has clear
return values or side effects so the import is meaningful and quiet.

In `@raygui/raygui.go`:
- Around line 21-27: PropertyValue.AsColor currently constructs rl.Color
positionally and NewColorPropertyValue packs channels with +; update AsColor to
use a keyed struct literal (rl.Color{R: uint8(v >> 24), G: uint8(v >> 16), B:
uint8(v >> 8), A: uint8(v)}) and change NewColorPropertyValue to use bitwise OR
for packing (e.g. PropertyValue(uint32(color.R)<<24 | uint32(color.G)<<16 |
uint32(color.B)<<8 | uint32(color.A))) so the intent is explicit and safe
against field reordering or width issues; apply these changes in the
PropertyValue.AsColor and NewColorPropertyValue functions.
- Around line 17-19: Extract the magic number 16 into a named constant and use
it wherever the extended-property base is referenced: define something like
MaxBaseProperties PropertyID = 16 and replace the literal in the
PropertyID.IsExtended method (currently p >= 16) and in the PropertyID const
block(s) that use `PropertyID = 16 + iota` so both the check in IsExtended and
the extended-property base definitions reference the same constant.

In `@wasm-runtime/rl/defines_gen_formatted.go`:
- Around line 21-97: The generated color variable names mix casing (e.g.
Darkgray, Darkblue, Darkpurple, Darkbrown, Skyblue, Raywhite vs DarkGreen), so
update the generator template that emits these vars to consistently use
CamelCase for compound names (make Darkgray→DarkGray, Darkblue→DarkBlue,
Darkpurple→DarkPurple, Darkbrown→DarkBrown, Skyblue→SkyBlue, Raywhite→RayWhite)
so all exported Color vars follow the same idiomatic Go casing (like DarkGreen);
change the naming logic in the generator where it formats identifiers for
variables such as Lightgray, Darkgreen, Skyblue, Raywhite to produce the
corrected CamelCase forms.

In `@wasm-runtime/rl/go.mod`:
- Around line 1-3: The go.mod in the nested module currently declares an
unqualified module name "module rl", which prevents other modules from importing
it and breaks the parent module's codegen expectations (templates.go writing
into rl/); change the module declaration in wasm-runtime/rl/go.mod to a fully
qualified import path (for example github.com/YourOrg/YourRepo/wasm-runtime/rl)
so it can be imported by canonical path, and add a short note in the repo README
or the PR description explaining the nested-module layout and whether consumers
should use a replace directive for local development.

In `@wasm-runtime/templates/aliases.go.gotmpl`:
- Line 1: The template sets an unused variable "$root" ({{- $root := . -}}) in
aliases.go.gotmpl, enums.go.gotmpl, funcimport.go.gotmpl and defines.go.gotmpl;
either remove that assignment from each template to eliminate the dead binding
or actually reference "$root" where cross-scope access is required (e.g., inside
a {{range}} or {{with}} block) so the root context is used instead of ".";
update the templates accordingly by deleting the "$root" line when not needed or
replacing local uses of "." inside nested blocks with "$root" to access the
top-level context.

In `@wasm-runtime/templates/enums.go.gotmpl`:
- Around line 14-23: The template emits empty comment lines when .Description is
blank; update the enums template to wrap the generated comment blocks with
presence checks: use {{- if .Description}}// {{ .Description}}{{- end}} for the
enum description (iterating .Enums) and similarly wrap the value comments inside
the .Values loop so no bare "//" is emitted; apply the same guard pattern to the
aliases template (guard its .Description) and to funcimport ensure you both
guard the description and include the missing "// " prefix before the
description when present.

In `@wasm-runtime/templates/templates.go`:
- Around line 29-57: Extract the shared template load+execute+write logic from
GenerateCode and GenerateCodeFormatted into a helper (e.g.,
renderAndWriteTemplate or writeTemplateOutput) that takes model, templ, name,
funcs and a postProcess function (nil for no-op or a formatter wrapper that
returns formatted bytes or an error); call LoadTemplateFuncs and Execute once in
that helper and use it from both GenerateCode and GenerateCodeFormatted; in the
formatted path, if format.Source returns an error, ensure the helper writes the
unformatted buffer to rl/{name}_gen_unformatted.go (or include the unformatted
bytes in the panic/error message) before returning or panicking so the raw
output is preserved for debugging.
- Around line 19-25: Update the stale doc comment for LoadTemplateFuncs to refer
to its actual name and behavior (mention it registers the provided
template.FuncMap and panics if parsing fails) and likewise update the doc
comment for GenerateCodeFormatted to reference GenerateCodeFormatted (not
GenerateCode) and describe that it writes formatted output to the
*_gen_formatted.go path; locate the comments above the LoadTemplateFuncs and
GenerateCodeFormatted function declarations and replace the copied text with
concise descriptions that include the function names and their specific
behaviors (registration of FuncMap for LoadTemplateFuncs and formatted output
path for GenerateCodeFormatted).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1460f218-fdcf-474f-ae55-a4c14678d1c6

📥 Commits

Reviewing files that changed from the base of the PR and between 27bc027 and ec1897a.

⛔ Files ignored due to path filters (3)
  • examples/go.sum is excluded by !**/*.sum
  • raygui/go.sum is excluded by !**/*.sum
  • raylib/go.sum is excluded by !**/*.sum
📒 Files selected for processing (26)
  • examples/go.mod
  • examples/raygui/main.go
  • index/index.html
  • index/index.js
  • index/runtime.js
  • raygui/go.mod
  • raygui/raygui.go
  • raylib/rcore_wasm.go
  • wasm-runtime/api/rlapi.go
  • wasm-runtime/api/rlapi.json
  • wasm-runtime/api/stringyvalue.go
  • wasm-runtime/go.mod
  • wasm-runtime/rl/.gitkeep
  • wasm-runtime/rl/aliases_gen_formatted.go
  • wasm-runtime/rl/defines_gen_formatted.go
  • wasm-runtime/rl/enums_gen_formatted.go
  • wasm-runtime/rl/funcimport_gen_unformatted.go
  • wasm-runtime/rl/go.mod
  • wasm-runtime/rl/structs_gen_formatted.go
  • wasm-runtime/templates/aliases.go.gotmpl
  • wasm-runtime/templates/defines.go.gotmpl
  • wasm-runtime/templates/enums.go.gotmpl
  • wasm-runtime/templates/funcimport.go.gotmpl
  • wasm-runtime/templates/structs.go.gotmpl
  • wasm-runtime/templates/templates.go
  • wasm-runtime/wasm.go
💤 Files with no reviewable changes (2)
  • index/index.html
  • raylib/rcore_wasm.go

Comment thread examples/go.mod
Comment on lines 5 to +17
replace github.com/gen2brain/raylib-go/raylib => ../raylib

require github.com/gen2brain/raylib-go/raylib v0.0.0-00010101000000-000000000000
replace github.com/gen2brain/raylib-go/raygui => ../raygui

require github.com/BrownNPC/wasm-ffi-go v1.1.0 // indirect
require (
github.com/gen2brain/raylib-go/raygui v0.0.0-00010101000000-000000000000
github.com/gen2brain/raylib-go/raylib v0.0.0-00010101000000-000000000000
)

require (
github.com/BrownNPC/Raylib-Go-Wasm/raylib v0.0.0-20260126231012-27bc0271203c // indirect
github.com/BrownNPC/wasm-ffi-go v1.2.0 // indirect
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, check the current state of examples/go.mod
cat examples/go.mod

Repository: BrownNPC/Raylib-Go-Wasm

Length of output: 564


🏁 Script executed:

# Check raygui/go.mod to see what module path it declares
cat raygui/go.mod

Repository: BrownNPC/Raylib-Go-Wasm

Length of output: 454


🏁 Script executed:

# Check if wasm-runtime/go.mod exists and what it declares
cat wasm-runtime/go.mod 2>/dev/null || echo "wasm-runtime/go.mod not found"

Repository: BrownNPC/Raylib-Go-Wasm

Length of output: 132


🏁 Script executed:

# List directory structure to understand the repo layout
find . -maxdepth 3 -name "go.mod" -type f

Repository: BrownNPC/Raylib-Go-Wasm

Length of output: 163


🏁 Script executed:

# Check raylib/go.mod to verify its declared module path
cat raylib/go.mod

Repository: BrownNPC/Raylib-Go-Wasm

Length of output: 174


Fix module path mismatches in examples/go.mod.

examples/go.mod replaces gen2brain/raylib-go paths, but both ../raylib and ../raygui declare github.com/BrownNPC/Raylib-Go-Wasm/ module paths. Go only accepts replacements when the target module declares the same path being replaced—this configuration will fail. Additionally, raygui's internal replace for wasm-runtime is not inherited by examples, so the build will fetch the remote pseudo-version instead of the local directory.

Align examples/go.mod to use BrownNPC paths consistently:

🛠️ Proposed module wiring
-replace github.com/gen2brain/raylib-go/raylib => ../raylib
+replace github.com/BrownNPC/Raylib-Go-Wasm/raylib => ../raylib
 
-replace github.com/gen2brain/raylib-go/raygui => ../raygui
+replace github.com/BrownNPC/Raylib-Go-Wasm/raygui => ../raygui
+replace github.com/BrownNPC/Raylib-Go-Wasm/wasm-runtime => ../wasm-runtime
 
 require (
-	github.com/gen2brain/raylib-go/raygui v0.0.0-00010101000000-000000000000
-	github.com/gen2brain/raylib-go/raylib v0.0.0-00010101000000-000000000000
+	github.com/BrownNPC/Raylib-Go-Wasm/raygui v0.0.0-00010101000000-000000000000
+	github.com/BrownNPC/Raylib-Go-Wasm/raylib v0.0.0-00010101000000-000000000000
 )
 
 require (
-	github.com/BrownNPC/Raylib-Go-Wasm/raylib v0.0.0-20260126231012-27bc0271203c // indirect
 	github.com/BrownNPC/wasm-ffi-go v1.2.0 // indirect
 )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
replace github.com/gen2brain/raylib-go/raylib => ../raylib
require github.com/gen2brain/raylib-go/raylib v0.0.0-00010101000000-000000000000
replace github.com/gen2brain/raylib-go/raygui => ../raygui
require github.com/BrownNPC/wasm-ffi-go v1.1.0 // indirect
require (
github.com/gen2brain/raylib-go/raygui v0.0.0-00010101000000-000000000000
github.com/gen2brain/raylib-go/raylib v0.0.0-00010101000000-000000000000
)
require (
github.com/BrownNPC/Raylib-Go-Wasm/raylib v0.0.0-20260126231012-27bc0271203c // indirect
github.com/BrownNPC/wasm-ffi-go v1.2.0 // indirect
)
replace github.com/BrownNPC/Raylib-Go-Wasm/raylib => ../raylib
replace github.com/BrownNPC/Raylib-Go-Wasm/raygui => ../raygui
replace github.com/BrownNPC/Raylib-Go-Wasm/wasm-runtime => ../wasm-runtime
require (
github.com/BrownNPC/Raylib-Go-Wasm/raygui v0.0.0-00010101000000-000000000000
github.com/BrownNPC/Raylib-Go-Wasm/raylib v0.0.0-00010101000000-000000000000
)
require (
github.com/BrownNPC/wasm-ffi-go v1.2.0 // indirect
)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/go.mod` around lines 5 - 17, The replaces in examples/go.mod point
at ../raylib and ../raygui but those local modules declare
github.com/BrownNPC/Raylib-Go-Wasm/* paths, so update the replace directives to
target github.com/BrownNPC/Raylib-Go-Wasm/raylib and
github.com/BrownNPC/Raylib-Go-Wasm/raygui (instead of github.com/gen2brain/...),
and also add a local replace for the wasm-runtime module used by raygui (the
wasm-runtime module path referenced inside the raygui module) to point at the
local directory (e.g., ../wasm-runtime) so the examples build uses the local
pseudo-modules rather than fetching remote pseudo-versions.

Comment thread examples/raygui/main.go
Comment on lines +5 to +8
import (
"github.com/gen2brain/raylib-go/raygui"
rl "github.com/gen2brain/raylib-go/raylib"
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm the import paths actually present in this fork's raygui and raylib packages.
fd -t f 'go.mod$' | xargs -I{} sh -c 'echo "== {} =="; head -3 {}'
echo '---'
rg -nP '^package\s+raygui|^package\s+rl' -g '*.go'

Repository: BrownNPC/Raylib-Go-Wasm

Length of output: 1066


🏁 Script executed:

# Check the actual content of examples/raygui/main.go, focusing on imports
cat -n examples/raygui/main.go | head -30

Repository: BrownNPC/Raylib-Go-Wasm

Length of output: 557


🏁 Script executed:

# Search for SetMain function in the raylib package of this fork
rg -n 'func.*SetMain|SetMain\s*=' raylib/ -A 3

Repository: BrownNPC/Raylib-Go-Wasm

Length of output: 274


Example imports the wrong module — uses upstream gen2brain/raylib-go instead of this fork.

This PR's whole point is adding raygui bindings under BrownNPC/Raylib-Go-Wasm, and the example must import from that fork, not the upstream package. The example currently imports github.com/gen2brain/raylib-go/raygui and github.com/gen2brain/raylib-go/raylib but calls rl.SetMain() on line 18, which is defined only in this fork's raylib/raylib_wasm.go. The upstream gen2brain/raylib-go does not have SetMain, so this file will not compile with the current imports.

🛠 Suggested fix
 import (
-    "github.com/gen2brain/raylib-go/raygui"
-    rl "github.com/gen2brain/raylib-go/raylib"
+    "github.com/BrownNPC/Raylib-Go-Wasm/raygui"
+    rl "github.com/BrownNPC/Raylib-Go-Wasm/raylib"
 )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import (
"github.com/gen2brain/raylib-go/raygui"
rl "github.com/gen2brain/raylib-go/raylib"
)
import (
"github.com/BrownNPC/Raylib-Go-Wasm/raygui"
rl "github.com/BrownNPC/Raylib-Go-Wasm/raylib"
)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/raygui/main.go` around lines 5 - 8, The example imports the upstream
gen2brain packages but calls rl.SetMain (defined only in this fork); update the
import statements in main.go to use the forked module (replace
github.com/gen2brain/raylib-go/raygui and github.com/gen2brain/raylib-go/raylib
with the BrownNPC/Raylib-Go-Wasm equivalents) so rl.SetMain and the raygui
bindings resolve correctly; locate the import block and references to
rl.SetMain/ raygui in the file (main.go) and change the package paths to the
forked repository.

Comment thread index/index.js
Comment on lines +41 to +47
WebAssembly.instantiateStreaming(fetch("main.wasm"), go.importObject).then(
(result) => {
const instance = result.instance;
globalThis.goInstance = instance;
go.run(instance);
},
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Missing error handler on WebAssembly.instantiateStreaming.

The .catch(console.error) that existed before was removed, and the new .then(...) chain has no rejection handler. If main.wasm fails to fetch/compile/instantiate (bad MIME type, 404, link error), the resulting unhandled promise rejection will be hard to diagnose — the page will simply not start with no visible log.

🛡️ Suggested fix
-WebAssembly.instantiateStreaming(fetch("main.wasm"), go.importObject).then(
-  (result) => {
-    const instance = result.instance;
-    globalThis.goInstance = instance;
-    go.run(instance);
-  },
-);
+WebAssembly.instantiateStreaming(fetch("main.wasm"), go.importObject)
+  .then((result) => {
+    const instance = result.instance;
+    globalThis.goInstance = instance;
+    go.run(instance);
+  })
+  .catch((err) => {
+    console.error("Failed to load main.wasm:", err);
+  });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
WebAssembly.instantiateStreaming(fetch("main.wasm"), go.importObject).then(
(result) => {
const instance = result.instance;
globalThis.goInstance = instance;
go.run(instance);
},
);
WebAssembly.instantiateStreaming(fetch("main.wasm"), go.importObject)
.then((result) => {
const instance = result.instance;
globalThis.goInstance = instance;
go.run(instance);
})
.catch((err) => {
console.error("Failed to load main.wasm:", err);
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@index/index.js` around lines 41 - 47, The promise from
WebAssembly.instantiateStreaming(fetch("main.wasm"), go.importObject) lacks a
rejection handler; update the chain where WebAssembly.instantiateStreaming is
called so it handles errors (e.g., add a .catch) and logs the error with context
before failing gracefully; ensure the handler references the same scope that
sets globalThis.goInstance and calls go.run so failures to
fetch/compile/instantiate are logged (include identifying symbols:
WebAssembly.instantiateStreaming, fetch("main.wasm"), globalThis.goInstance,
go.run).

Comment thread index/runtime.js
Comment on lines +69 to +77
CStringGetLength = (sp) => {
sp >>>= 0;
const cStr = this.getInt32(sp + 8 * 1);
const view = raylib.HEAPU8.subarray(cStr);
let len = 0;
while (view[len] !== 0) {
len++;
}
this.setInt32(sp + 8 * 2, len);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

find . -name "runtime.js" -type f

Repository: BrownNPC/Raylib-Go-Wasm

Length of output: 85


🏁 Script executed:

git ls-files | grep -E "index|runtime"

Repository: BrownNPC/Raylib-Go-Wasm

Length of output: 817


🏁 Script executed:

wc -l ./index/runtime.js

Repository: BrownNPC/Raylib-Go-Wasm

Length of output: 89


🏁 Script executed:

sed -n '60,85p' ./index/runtime.js

Repository: BrownNPC/Raylib-Go-Wasm

Length of output: 814


🏁 Script executed:

node <<'EOF'
// Test 1: Verify what happens when accessing out-of-bounds index in TypedArray
const buffer = new ArrayBuffer(10);
const view = new Uint8Array(buffer);

console.log("=== Test 1: Out-of-bounds access ===");
console.log("view.length:", view.length);
console.log("view[5]:", view[5]);
console.log("view[10]:", view[10]);  // Out of bounds
console.log("view[100]:", view[100]); // Way out of bounds
console.log("typeof view[100]:", typeof view[100]);
console.log("view[100] !== 0:", view[100] !== 0);
console.log("undefined !== 0:", undefined !== 0);

// Test 2: Verify subarray behavior
console.log("\n=== Test 2: subarray behavior ===");
const subview = view.subarray(5);
console.log("subview.length:", subview.length);
console.log("subview[0]:", subview[0]);
console.log("subview[10]:", subview[10]); // Out of bounds
console.log("subview[10] !== 0:", subview[10] !== 0);

// Test 3: Verify indexOf behavior
console.log("\n=== Test 3: indexOf on TypedArray ===");
const testBuffer = new ArrayBuffer(20);
const testView = new Uint8Array(testBuffer);
testView[5] = 0;  // Set NUL at index 5
console.log("indexOf result with NUL present:", testView.indexOf(0));
console.log("indexOf result without NUL:", new Uint8Array(new ArrayBuffer(10)).indexOf(0));

// Test 4: String scanning simulation (like in the code)
console.log("\n=== Test 4: Infinite loop scenario ===");
const scanBuffer = new ArrayBuffer(5);
const scanView = new Uint8Array(scanBuffer);
// No NUL terminator set - all zeros initially, but simulate invalid pointer
let len = 0;
let iterations = 0;
const maxIterations = 100;
// This simulates accessing beyond bounds
while (len < 1000 && iterations < maxIterations) {
  if (scanView[len] !== 0) {
    break;
  }
  len++;
  iterations++;
}
console.log("Iterations before hitting maxIterations:", iterations);
console.log("Final len value:", len);
console.log("Last accessed value:", scanView[len]);
EOF

Repository: BrownNPC/Raylib-Go-Wasm

Length of output: 594


🏁 Script executed:

node <<'EOF'
// Test the proposed fix behavior
console.log("=== Testing proposed fix (indexOf approach) ===");

// Test 1: Valid case with NUL terminator
const buffer1 = new ArrayBuffer(10);
const view1 = new Uint8Array(buffer1);
view1[3] = 65; // 'A'
view1[4] = 0;  // NUL terminator at index 4
const subview1 = view1.subarray(3);
console.log("Test 1 (valid string at offset 3):");
console.log("  indexOf(0):", subview1.indexOf(0));

// Test 2: No NUL terminator
const buffer2 = new ArrayBuffer(10);
const view2 = new Uint8Array(buffer2);
view2[3] = 65;
view2[4] = 66;
const subview2 = view2.subarray(3);
console.log("\nTest 2 (no NUL terminator):");
console.log("  indexOf(0):", subview2.indexOf(0));

// Test 3: subarray with out-of-bounds start index
console.log("\nTest 3 (subarray with out-of-bounds start):");
const buffer3 = new ArrayBuffer(10);
const view3 = new Uint8Array(buffer3);
try {
  const subview3 = view3.subarray(50); // Way out of bounds
  console.log("  subarray created successfully");
  console.log("  subview.length:", subview3.length);
  console.log("  indexOf(0):", subview3.indexOf(0));
} catch (e) {
  console.log("  Error:", e.message);
}

// Test 4: Negative subarray index
console.log("\nTest 4 (negative subarray index):");
const buffer4 = new ArrayBuffer(10);
const view4 = new Uint8Array(buffer4);
try {
  const subview4 = view4.subarray(-5);
  console.log("  subarray created successfully");
  console.log("  subview.length:", subview4.length);
} catch (e) {
  console.log("  Error:", e.message);
}

// Test 5: Real-world scenario - cStr as large invalid pointer
console.log("\nTest 5 (cStr = 999999, buffer size = 1000):");
const buffer5 = new ArrayBuffer(1000);
const view5 = new Uint8Array(buffer5);
const subview5 = view5.subarray(999999);
console.log("  subview.length:", subview5.length);
console.log("  indexOf(0):", subview5.indexOf(0));
EOF

Repository: BrownNPC/Raylib-Go-Wasm

Length of output: 493


Bound the C-string scan to prevent infinite loops when scanning unterminated or invalid C strings.

The current implementation can hang indefinitely if cStr is invalid or the string lacks a NUL terminator. Accessing beyond the bounds of a Uint8Array returns undefined, and the condition undefined !== 0 evaluates to true, causing the loop to continue forever.

🛡️ Proposed fix
   CStringGetLength = (sp) => {
     sp >>>= 0;
     const cStr = this.getInt32(sp + 8 * 1);
-    const view = raylib.HEAPU8.subarray(cStr);
-    let len = 0;
-    while (view[len] !== 0) {
-      len++;
+    const view = globalThis.raylib.HEAPU8.subarray(cStr);
+    const len = view.indexOf(0);
+    if (len === -1) {
+      throw new Error(`CStringGetLength: unterminated C string at ${cStr}`);
     }
     this.setInt32(sp + 8 * 2, len);
   };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@index/runtime.js` around lines 69 - 77, The CStringGetLength implementation
can loop forever when cStr is invalid or a NUL is missing; fix it by bounding
the scan to the HEAPU8 buffer length and validating cStr before accessing
memory: ensure cStr is a non-negative integer less than raylib.HEAPU8.length,
then iterate while index < raylib.HEAPU8.length and view[index] !== 0,
incrementing len; if cStr is out of range treat length as 0 (or clamp to
available bytes) and then call this.setInt32(sp + 8 * 2, len). Also reference
the variables CStringGetLength, sp, cStr, raylib.HEAPU8, view and this.setInt32
when making the change.

Comment thread raygui/raygui.go
Comment on lines +186 to +189
const (
SPINNER_BUTTON_WIDTH PropertyID = 16 // Spinner left/right buttons width
SPINNER_BUTTON_SPACING // Spinner buttons separation
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Bug: SPINNER_BUTTON_SPACING collides with SPINNER_BUTTON_WIDTH (both = 16).

Because the first line uses PropertyID = 16 (no iota), Go's const-repetition rule repeats the literal 16 for subsequent untyped lines. So SPINNER_BUTTON_SPACING evaluates to 16, not 17. Every other extended-property block in this file correctly uses 16 + iota (see COMBO_BUTTON_WIDTH/COMBO_BUTTON_SPACING at lines 168-169, ARROWS_SIZE..SCROLL_SPEED at lines 153-158, etc.).

Any style lookup using SPINNER_BUTTON_SPACING will silently resolve to the same slot as SPINNER_BUTTON_WIDTH, producing incorrect spinner geometry at runtime with no compile-time error.

🐛 Proposed fix
 // ValueBox/Spinner
 const (
-	SPINNER_BUTTON_WIDTH   PropertyID = 16 // Spinner left/right buttons width
-	SPINNER_BUTTON_SPACING                 // Spinner buttons separation
+	SPINNER_BUTTON_WIDTH   PropertyID = 16 + iota // Spinner left/right buttons width
+	SPINNER_BUTTON_SPACING                        // Spinner buttons separation
 )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const (
SPINNER_BUTTON_WIDTH PropertyID = 16 // Spinner left/right buttons width
SPINNER_BUTTON_SPACING // Spinner buttons separation
)
const (
SPINNER_BUTTON_WIDTH PropertyID = 16 + iota // Spinner left/right buttons width
SPINNER_BUTTON_SPACING // Spinner buttons separation
)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@raygui/raygui.go` around lines 186 - 189, The two constants collide because
SPINNER_BUTTON_WIDTH is set to the literal 16 so the following untyped
SPINNER_BUTTON_SPACING repeats 16; change the const block to use iota like other
blocks by making SPINNER_BUTTON_WIDTH of type PropertyID equal to 16 + iota
(e.g., "SPINNER_BUTTON_WIDTH PropertyID = 16 + iota") and leave
SPINNER_BUTTON_SPACING untyped so it becomes 17; update the const block
containing SPINNER_BUTTON_WIDTH and SPINNER_BUTTON_SPACING accordingly.

Comment thread wasm-runtime/go.mod
@@ -0,0 +1,3 @@
module github.com/BrownNPC/Raylib-Go-Wasm/wasm-runtime

go 1.26.1
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

Latest stable Go release version 2026

💡 Result:

The latest stable Go release as of 2026-04-21 is go1.26.2, released on 2026-04-07. It includes security fixes and bug fixes across various packages. This is confirmed by the official Go download page featuring go1.26.2 binaries and the release history listing it as the most recent minor release in the 1.26 series. No newer stable release (e.g., 1.26.3 or 1.27) is available in the sources.

Citations:


🏁 Script executed:

find . -name "go.mod" -path "*rl*" | head -20

Repository: BrownNPC/Raylib-Go-Wasm

Length of output: 91


🏁 Script executed:

cat wasm-runtime/rl/go.mod 2>/dev/null | head -5 || echo "File not found or error reading"

Repository: BrownNPC/Raylib-Go-Wasm

Length of output: 88


Align Go versions across the two modules or document the difference.

This module declares go 1.26.1, while wasm-runtime/rl/go.mod declares go 1.25.6. Either:

  1. Align the go directives to the same version, or
  2. Document why they differ.

Without alignment, builds will impose different minimum toolchain requirements on contributors, and CI caching/toolchain auto-download behavior may diverge between modules.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wasm-runtime/go.mod` at line 3, Two go.mod files declare different go
directives ("go 1.26.1" vs "go 1.25.6"), so either make them consistent or
document the deliberate mismatch; pick the project-wide toolchain version you
want and update the go directive in the other module to match the chosen value
(change the "go 1.26.1" or "go 1.25.6" entry accordingly), then run go mod tidy
and verify local/CI builds; if you intentionally need different versions, add a
short note to the repo README explaining why the go directives differ and which
module requires which toolchain.

Comment thread wasm-runtime/wasm.go Outdated
Comment on lines +98 to +128
func copySliceToC[Slice ~[]E, E any](s Slice) (Cptr, func()) {
// size of the slice's underlying array in bytes
sliceSize := Cptr(unsafe.Sizeof(s[:1][0])) * Cptr(len(s))
// allocate C array to hold Value
dstCptr := malloc(sliceSize)
// copy underlying array memory to C
_copyToC(unsafe.Pointer(unsafe.SliceData(s)), sliceSize, dstCptr)
return dstCptr, func() { free(dstCptr) }
}

// copyValueToGo copies a value from C memory to Go memory.
// Useful for copying structs
//
// NOTE: Slices are not supported. Use [copySliceToGo]
func copyValueToGo[T any](src Cptr, dst *T) {
size := sizeof(*dst)
_copyToGo(unsafe.Pointer(dst), size, src)
}

// copySliceToGo copies a C array into a Go Slice.
//
// It copies bytes to the underlying array of the slice.
func copySliceToGo[Slice ~[]E, E any](src Cptr, dst Slice) {
// size of underlying array
var occupiedSize = len(dst)
if occupiedSize == 0 {
occupiedSize = cap(dst)
}
size := Cptr(unsafe.Sizeof(dst[0])) * Cptr(occupiedSize)
dstPtr := unsafe.SliceData(dst)
_copyToGo(unsafe.Pointer(dstPtr), size, src)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -name "wasm.go" -type f

Repository: BrownNPC/Raylib-Go-Wasm

Length of output: 89


🏁 Script executed:

git ls-files | grep -i wasm | head -20

Repository: BrownNPC/Raylib-Go-Wasm

Length of output: 718


🏁 Script executed:

wc -l ./wasm-runtime/wasm.go

Repository: BrownNPC/Raylib-Go-Wasm

Length of output: 93


🏁 Script executed:

sed -n '95,135p' ./wasm-runtime/wasm.go | cat -n

Repository: BrownNPC/Raylib-Go-Wasm

Length of output: 1652


🏁 Script executed:

cat > /tmp/test_empty_slice.go << 'EOF'
package main

import (
	"fmt"
)

func main() {
	// Test 1: empty slice indexing
	var s []int
	fmt.Println("len(s):", len(s), "cap(s):", cap(s))
	
	// Test 2: zero-length slice with capacity
	s2 := make([]int, 0, 10)
	fmt.Println("len(s2):", len(s2), "cap(s2):", cap(s2))
	
	// Test 3: Try to index empty slice
	defer func() {
		if r := recover(); r != nil {
			fmt.Println("Panic on empty slice indexing:", r)
		}
	}()
	
	_ = s[0]
}
EOF
go run /tmp/test_empty_slice.go 2>&1 || true

Repository: BrownNPC/Raylib-Go-Wasm

Length of output: 192


🏁 Script executed:

cat > /tmp/test_unsafe.go << 'EOF'
package main

import (
	"fmt"
	"unsafe"
)

func main() {
	// Test 1: unsafe.Sizeof on zero-value
	var zero int
	fmt.Println("sizeof(zero):", unsafe.Sizeof(zero))
	
	// Test 2: unsafe.SliceData on empty slice
	s := make([]int, 0, 10)
	ptr := unsafe.SliceData(s)
	fmt.Println("ptr on empty slice with cap:", ptr)
	
	// Test 3: unsafe.SliceData on nil slice
	var nilSlice []int
	ptr2 := unsafe.SliceData(nilSlice)
	fmt.Println("ptr on nil slice:", ptr2)
}
EOF
go run /tmp/test_unsafe.go 2>&1 || true

Repository: BrownNPC/Raylib-Go-Wasm

Length of output: 150


🏁 Script executed:

rg -n "copySliceToC|copySliceToGo" ./wasm-runtime/ -A 2 -B 2

Repository: BrownNPC/Raylib-Go-Wasm

Length of output: 1915


🏁 Script executed:

rg -n "copySliceToC|copySliceToGo" . -A 2 -B 2 --type go

Repository: BrownNPC/Raylib-Go-Wasm

Length of output: 1915


🏁 Script executed:

cat > /tmp/verify_fix.go << 'EOF'
package main

import (
	"fmt"
	"unsafe"
)

// Test the proposed fix approach
func copySliceToC_fixed[Slice ~[]E, E any](s Slice) (uintptr, func()) {
	if len(s) == 0 {
		return 0, func() {}
	}
	var zero E
	// size of the slice's underlying array in bytes
	sliceSize := uintptr(unsafe.Sizeof(zero)) * uintptr(len(s))
	fmt.Printf("copySliceToC_fixed: sliceSize=%d\n", sliceSize)
	return sliceSize, func() {}
}

func copySliceToGo_fixed[Slice ~[]E, E any](dst Slice) {
	if len(dst) == 0 {
		return
	}
	var zero E
	size := uintptr(unsafe.Sizeof(zero)) * uintptr(len(dst))
	fmt.Printf("copySliceToGo_fixed: size=%d (len=%d)\n", size, len(dst))
}

func main() {
	// Test with empty slice
	var emptySlice []int
	ptr1, cleanup1 := copySliceToC_fixed(emptySlice)
	fmt.Println("Empty slice to C:", ptr1)
	cleanup1()

	// Test with non-empty slice
	nonEmptySlice := make([]int, 5)
	ptr2, cleanup2 := copySliceToC_fixed(nonEmptySlice)
	fmt.Println("Non-empty slice to C:", ptr2)
	cleanup2()

	// Test with empty slice to Go
	var emptySliceGo []int
	copySliceToGo_fixed(emptySliceGo)

	// Test with non-empty slice to Go
	nonEmptySliceGo := make([]int, 5)
	copySliceToGo_fixed(nonEmptySliceGo)
}
EOF
go run /tmp/verify_fix.go

Repository: BrownNPC/Raylib-Go-Wasm

Length of output: 181


Handle zero-length slices to prevent index panics.

Both copySliceToC and copySliceToGo will panic when passed empty slices because they index element 0 without length checks. Additionally, copySliceToGo attempts to use capacity when length is zero, which writes data that callers cannot safely access without reslicing.

Proposed fix
 func copySliceToC[Slice ~[]E, E any](s Slice) (Cptr, func()) {
+	if len(s) == 0 {
+		return 0, func() {}
+	}
+	var zero E
 	// size of the slice's underlying array in bytes
-	sliceSize := Cptr(unsafe.Sizeof(s[:1][0])) * Cptr(len(s))
+	sliceSize := Cptr(unsafe.Sizeof(zero)) * Cptr(len(s))
 	// allocate C array to hold Value
 	dstCptr := malloc(sliceSize)
 	// copy underlying array memory to C
 	_copyToC(unsafe.Pointer(unsafe.SliceData(s)), sliceSize, dstCptr)
 	return dstCptr, func() { free(dstCptr) }
 }

 func copySliceToGo[Slice ~[]E, E any](src Cptr, dst Slice) {
-	// size of underlying array
-	var occupiedSize = len(dst)
-	if occupiedSize == 0 {
-		occupiedSize = cap(dst)
-	}
-	size := Cptr(unsafe.Sizeof(dst[0])) * Cptr(occupiedSize)
+	if len(dst) == 0 {
+		return
+	}
+	var zero E
+	size := Cptr(unsafe.Sizeof(zero)) * Cptr(len(dst))
 	dstPtr := unsafe.SliceData(dst)
 	_copyToGo(unsafe.Pointer(dstPtr), size, src)
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func copySliceToC[Slice ~[]E, E any](s Slice) (Cptr, func()) {
// size of the slice's underlying array in bytes
sliceSize := Cptr(unsafe.Sizeof(s[:1][0])) * Cptr(len(s))
// allocate C array to hold Value
dstCptr := malloc(sliceSize)
// copy underlying array memory to C
_copyToC(unsafe.Pointer(unsafe.SliceData(s)), sliceSize, dstCptr)
return dstCptr, func() { free(dstCptr) }
}
// copyValueToGo copies a value from C memory to Go memory.
// Useful for copying structs
//
// NOTE: Slices are not supported. Use [copySliceToGo]
func copyValueToGo[T any](src Cptr, dst *T) {
size := sizeof(*dst)
_copyToGo(unsafe.Pointer(dst), size, src)
}
// copySliceToGo copies a C array into a Go Slice.
//
// It copies bytes to the underlying array of the slice.
func copySliceToGo[Slice ~[]E, E any](src Cptr, dst Slice) {
// size of underlying array
var occupiedSize = len(dst)
if occupiedSize == 0 {
occupiedSize = cap(dst)
}
size := Cptr(unsafe.Sizeof(dst[0])) * Cptr(occupiedSize)
dstPtr := unsafe.SliceData(dst)
_copyToGo(unsafe.Pointer(dstPtr), size, src)
func copySliceToC[Slice ~[]E, E any](s Slice) (Cptr, func()) {
if len(s) == 0 {
return 0, func() {}
}
var zero E
// size of the slice's underlying array in bytes
sliceSize := Cptr(unsafe.Sizeof(zero)) * Cptr(len(s))
// allocate C array to hold Value
dstCptr := malloc(sliceSize)
// copy underlying array memory to C
_copyToC(unsafe.Pointer(unsafe.SliceData(s)), sliceSize, dstCptr)
return dstCptr, func() { free(dstCptr) }
}
// copyValueToGo copies a value from C memory to Go memory.
// Useful for copying structs
//
// NOTE: Slices are not supported. Use [copySliceToGo]
func copyValueToGo[T any](src Cptr, dst *T) {
size := sizeof(*dst)
_copyToGo(unsafe.Pointer(dst), size, src)
}
// copySliceToGo copies a C array into a Go Slice.
//
// It copies bytes to the underlying array of the slice.
func copySliceToGo[Slice ~[]E, E any](src Cptr, dst Slice) {
if len(dst) == 0 {
return
}
var zero E
size := Cptr(unsafe.Sizeof(zero)) * Cptr(len(dst))
dstPtr := unsafe.SliceData(dst)
_copyToGo(unsafe.Pointer(dstPtr), size, src)
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wasm-runtime/wasm.go` around lines 98 - 128, copySliceToC and copySliceToGo
panic on empty slices because they access dst[0] without checking length and
copySliceToGo writes into capacity-only space; update both to handle zero-length
slices: in copySliceToC (function name copySliceToC) detect if len(s)==0 and
return a nil Cptr (zero value) with a no-op cleanup function (avoid malloc and
_copyToC), and in copySliceToGo (function name copySliceToGo) if len(dst)==0
simply return without copying (do not fall back to cap(dst) or write into the
underlying array), keeping copyValueToGo unchanged; ensure any callers can
accept a nil Cptr result from copySliceToC or skip calling copySliceToGo when
destination length is zero.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (4)
index/index.js (1)

42-48: ⚠️ Potential issue | 🟡 Minor

Handle main.wasm startup failures.

This promise chain still has no rejection handler, so a bad path, MIME issue, or instantiation error fails with little context.

Suggested fix
-WebAssembly.instantiateStreaming(fetch("main.wasm"), go.importObject).then(
-  (result) => {
-    const instance = result.instance;
-    globalThis.goInstance = instance;
-    go.run(instance);
-  },
-);
+WebAssembly.instantiateStreaming(fetch("main.wasm"), go.importObject)
+  .then((result) => {
+    const instance = result.instance;
+    globalThis.goInstance = instance;
+    go.run(instance);
+  })
+  .catch((err) => {
+    console.error("Failed to load main.wasm:", err);
+  });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@index/index.js` around lines 42 - 48, The promise from
WebAssembly.instantiateStreaming(fetch("main.wasm"), go.importObject) lacks a
rejection handler; add a .catch handler on that chain to log the error and
handle startup failure (e.g., clear or set globalThis.goInstance accordingly and
surface a user-friendly error or stop further initialization). Locate the call
to WebAssembly.instantiateStreaming and append a .catch that captures the thrown
error, calls your logger/console.error with contextual text plus the error, and
performs any cleanup or fallback so go.run(instance) isn't attempted on failure.
raygui/raygui.go (1)

190-193: ⚠️ Potential issue | 🔴 Critical

Use iota so spinner style IDs do not collide.

SPINNER_BUTTON_SPACING repeats the literal 16, so it aliases SPINNER_BUTTON_WIDTH instead of becoming 17.

Suggested fix
 const (
-	SPINNER_BUTTON_WIDTH   PropertyID = 16 // Spinner left/right buttons width
-	SPINNER_BUTTON_SPACING                 // Spinner buttons separation
+	SPINNER_BUTTON_WIDTH   PropertyID = 16 + iota // Spinner left/right buttons width
+	SPINNER_BUTTON_SPACING                        // Spinner buttons separation
 )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@raygui/raygui.go` around lines 190 - 193, The two constants
SPINNER_BUTTON_WIDTH and SPINNER_BUTTON_SPACING currently share the same value
because the second line repeats the literal 16; change the const block to use
iota so the next value increments (e.g., make SPINNER_BUTTON_WIDTH PropertyID
start at 16 using iota and let SPINNER_BUTTON_SPACING be the next iota value) to
prevent ID collisions between spinner style IDs.
wasm-runtime/wasm.go (1)

112-142: ⚠️ Potential issue | 🟠 Major

Handle zero-length slices before indexing.

CopySliceToC panics on s[:1][0] for empty slices, and CopySliceToGo still indexes dst[0] when len(dst) == 0. Also avoid copying into capacity-only space.

Suggested fix
 func CopySliceToC[Slice ~[]E, E any](s Slice) (Cptr, func()) {
+	if len(s) == 0 {
+		return 0, func() {}
+	}
+	var zero E
 	// size of the slice's underlying array in bytes
-	sliceSize := Cptr(unsafe.Sizeof(s[:1][0])) * Cptr(len(s))
+	sliceSize := Cptr(unsafe.Sizeof(zero)) * Cptr(len(s))
 	// allocate C array to hold Value
 	dstCptr := Malloc(sliceSize)
@@
 func CopySliceToGo[Slice ~[]E, E any](src Cptr, dst Slice) {
-	// size of underlying array
-	var occupiedSize = len(dst)
-	if occupiedSize == 0 {
-		occupiedSize = cap(dst)
-	}
-	size := Cptr(unsafe.Sizeof(dst[0])) * Cptr(occupiedSize)
+	if len(dst) == 0 {
+		return
+	}
+	var zero E
+	size := Cptr(unsafe.Sizeof(zero)) * Cptr(len(dst))
 	dstPtr := unsafe.SliceData(dst)
 	_copyToGo(unsafe.Pointer(dstPtr), size, src)
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wasm-runtime/wasm.go` around lines 112 - 142, CopySliceToC and CopySliceToGo
currently index into slices (s[:1][0] and dst[0]) which panics for zero-length
slices and causes CopySliceToGo to copy capacity instead of only the visible
length. Fix both functions by computing element size using a zero value of the
element type (e.g., var z E; elemSize := Cptr(unsafe.Sizeof(z))) instead of
indexing, and handle zero-length slices: in CopySliceToC if len(s)==0 return a
nil/zero Cptr and a no-op free function; in CopySliceToGo if len(dst)==0 return
immediately. Also change CopySliceToGo to use only len(dst) (not cap(dst)) when
calculating total bytes to copy so you only write to the slice's actual
underlying array.
index/runtime.js (1)

69-99: ⚠️ Potential issue | 🔴 Critical

Use direct //go:wasmimport arguments, not the wasm_exec.js stack frame.

These helpers are imported from Go with //go:wasmimport, so Go calls them with direct Wasm arguments. For example, CString(string) Cptr calls CStringFromGoString(saddr, len) and expects a returned Cptr; the current implementation treats saddr as sp and writes the result into Go memory instead. CopyToC, CopyToGo, Alert, and CStringGetLength have the same ABI mismatch. While changing CStringGetLength, also keep its scan bounded; the current loop can still hang on an unterminated C string.

Official Go docs for //go:wasmimport list the direct Wasm type mapping and say other parameter/result types are disallowed: https://pkg.go.dev/cmd/compile#hdr-WebAssembly_Directives

Suggested direction
-  CStringGetLength = (sp) => {
-    sp >>>= 0;
-    const cStr = this.getInt32(sp + 8 * 1);
-    const view = raylib.HEAPU8.subarray(cStr);
-    let len = 0;
-    while (view[len] !== 0) {
-      len++;
-    }
-    this.setInt32(sp + 8 * 2, len);
+  CStringGetLength = (cStr) => {
+    cStr >>>= 0;
+    if (cStr >= globalThis.raylib.HEAPU8.length) return 0;
+    const view = globalThis.raylib.HEAPU8.subarray(cStr);
+    const len = view.indexOf(0);
+    if (len === -1) {
+      throw new Error(`CStringGetLength: unterminated C string at ${cStr}`);
+    }
+    return len;
   };
@@
-  CStringFromGoString = (sp) => {
-    sp >>>= 0;
-    // get string addr and length
-    const saddr = this.getInt64(sp + 8 * 1); // go string address
-    const len = this.getInt64(sp + 8 * 2); // go string length
+  CStringFromGoString = (saddr, len) => {
+    saddr >>>= 0;
+    len >>>= 0;
@@
-    // return cstr
-    this.setInt32(sp + 8 * 3, cstr);
+    return cstr;
   };
@@
-  CopyToC = (sp) => {
-    sp >>>= 0;
-    const srcGoPtr = this.getInt64(sp + 8 * 1);
-    const srcSize = this.getInt32(sp + 8 * 2); // size of the dstGoPtr
-    // size and pointer are packed into a single 64bit int by Go's compiler
-    const dstCptr = this.getInt32(sp + 8 * 2 + 4);
-
+  CopyToC = (srcGoPtr, srcSize, dstCptr) => {
+    srcGoPtr >>>= 0;
+    srcSize >>>= 0;
+    dstCptr >>>= 0;
     const goBytes = this.getmem(srcGoPtr, srcSize);
     this.getRaylibU8Array(dstCptr, srcSize).set(goBytes);
   };
@@
-  CopyToGo = (sp) => {
-    sp >>>= 0;
-    const dstGoPtr = this.getInt64(sp + 8 * 1);
-    const size = this.getInt32(sp + 8 * 2); // size of the dstGoPtr
-    // size and pointer are packed into a single 64bit int by Go's compiler
-    const srcCptr = this.getInt32(sp + 8 * 2 + 4);
-
+  CopyToGo = (dstGoPtr, size, srcCptr) => {
+    dstGoPtr >>>= 0;
+    size >>>= 0;
+    srcCptr >>>= 0;
     const srcCBytes = this.getRaylibU8Array(srcCptr, size);
     const dstGoBytes = this.getmem(dstGoPtr, size);
@@
-  Alert = (sp) => {
-    sp >>>= 0;
-    const saddr = this.getInt64(sp + 8 * 1);
-    const len = this.getInt64(sp + 8 * 2);
+  Alert = (saddr, len) => {
+    saddr >>>= 0;
+    len >>>= 0;
     const strU8 = this.getmem(saddr, len);

Run this read-only check to compare the Go import signatures with the JS helper arities:

#!/bin/bash
# Description: Inspect Go `//go:wasmimport gojs` declarations and corresponding JS runtime helpers.
# Expected: JS helpers should accept direct parameters matching the Go imports, not a single `sp`.

rg -n -C2 '//go:wasmimport gojs (CStringFromGoString|CStringGetLength|CopyToC|CopyToGo|Alert)|func (CString|_cStringGetLength|_copyToC|_copyToGo|Alert)\b' wasm-runtime/wasm.go
rg -n -C2 'CStringGetLength|CStringFromGoString|CopyToC|CopyToGo|Alert' index/runtime.js

Also applies to: 110-155

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@index/runtime.js` around lines 69 - 99, The imported helpers currently assume
the wasm_exec.js stack-frame ABI (single sp parameter) but Go uses
//go:wasmimport with direct Wasm arguments; update CStringFromGoString,
CStringGetLength (and similarly CopyToC, CopyToGo, Alert) to accept the direct
parameters the Go imports declare (e.g., CStringFromGoString(saddr, len) should
take saddr and len, malloc/copy the bytes and return the C pointer as the
function return value instead of writing into Go memory), and change
CStringGetLength to accept a pointer argument (e.g., cstr) and return the length
as an integer while bounding the scan (stop after a safe max or using a provided
length) to avoid hanging on unterminated strings; locate the current
implementations named CStringFromGoString and CStringGetLength and replace the
sp-based signature/behavior with direct-arg/return semantics and bounded
scanning.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@raygui/raygui.go`:
- Around line 596-603: The wasm-imported function signatures that use
struct-by-value types (e.g., guiGetColor returning rl.Color) must be replaced
with pointer/scalar shims: change the //go:wasmimport declarations (like
guiGetColor) to accept an unsafe.Pointer (or individual scalar fields / packed
uint32) as an out-parameter or scalars for each field, then implement Go wrapper
functions (e.g., GetColor) that call the shim and reconstruct the rl.Color value
from the memory/returned scalars; apply the same pattern for all other imports
that currently pass/return rl.Color, rl.Rectangle, rl.Vector2, rl.Vector3,
color.RGBA (create shims named clearly per original like guiGetRectangle_shim or
guiGetColor_shim), use unsafe.Pointer to write/read struct bytes or pack/unpack
fields into uint32/uint64 scalars, and keep the original exported wrapper
signatures (GetColor, etc.) unchanged so callers continue to receive struct
values.
- Around line 814-875: The wrappers (Toggle, ToggleGroup, ToggleSlider) call the
native functions (guiToggle, guiToggleGroup, guiToggleSlider) but ignore their
return values and instead return the original input; capture each native call's
return and return that converted value (e.g. for Toggle use the int32 result
from guiToggle and convert with wasm.GoBool instead of returning the original
cactive, for ToggleGroup and ToggleSlider return the int32 result from
guiToggleGroup/guiToggleSlider directly). Update the code paths referencing
wasm.CBool/wasm.GoBool and the int32 cactive variables in Toggle, ToggleGroup,
and ToggleSlider so the functions return the updated control state from the
native call.
- Around line 1587-1595: SetTooltip currently allocates a C string, calls
guiSetTooltip, then immediately frees it, leaving raygui with a dangling
pointer; instead keep the C string alive by storing it in a package-level
variable (e.g., tooltipPtr) that persists across frames. Change SetTooltip to:
allocate ctooltip with wasm.CString, free any previous tooltipPtr if non-nil,
assign tooltipPtr = ctooltip, and then call guiSetTooltip(tooltipPtr); ensure
guiSetTooltip and tooltipPtr are the referenced symbols used so the pointer
lifetime is maintained until explicitly replaced or freed.

---

Duplicate comments:
In `@index/index.js`:
- Around line 42-48: The promise from
WebAssembly.instantiateStreaming(fetch("main.wasm"), go.importObject) lacks a
rejection handler; add a .catch handler on that chain to log the error and
handle startup failure (e.g., clear or set globalThis.goInstance accordingly and
surface a user-friendly error or stop further initialization). Locate the call
to WebAssembly.instantiateStreaming and append a .catch that captures the thrown
error, calls your logger/console.error with contextual text plus the error, and
performs any cleanup or fallback so go.run(instance) isn't attempted on failure.

In `@index/runtime.js`:
- Around line 69-99: The imported helpers currently assume the wasm_exec.js
stack-frame ABI (single sp parameter) but Go uses //go:wasmimport with direct
Wasm arguments; update CStringFromGoString, CStringGetLength (and similarly
CopyToC, CopyToGo, Alert) to accept the direct parameters the Go imports declare
(e.g., CStringFromGoString(saddr, len) should take saddr and len, malloc/copy
the bytes and return the C pointer as the function return value instead of
writing into Go memory), and change CStringGetLength to accept a pointer
argument (e.g., cstr) and return the length as an integer while bounding the
scan (stop after a safe max or using a provided length) to avoid hanging on
unterminated strings; locate the current implementations named
CStringFromGoString and CStringGetLength and replace the sp-based
signature/behavior with direct-arg/return semantics and bounded scanning.

In `@raygui/raygui.go`:
- Around line 190-193: The two constants SPINNER_BUTTON_WIDTH and
SPINNER_BUTTON_SPACING currently share the same value because the second line
repeats the literal 16; change the const block to use iota so the next value
increments (e.g., make SPINNER_BUTTON_WIDTH PropertyID start at 16 using iota
and let SPINNER_BUTTON_SPACING be the next iota value) to prevent ID collisions
between spinner style IDs.

In `@wasm-runtime/wasm.go`:
- Around line 112-142: CopySliceToC and CopySliceToGo currently index into
slices (s[:1][0] and dst[0]) which panics for zero-length slices and causes
CopySliceToGo to copy capacity instead of only the visible length. Fix both
functions by computing element size using a zero value of the element type
(e.g., var z E; elemSize := Cptr(unsafe.Sizeof(z))) instead of indexing, and
handle zero-length slices: in CopySliceToC if len(s)==0 return a nil/zero Cptr
and a no-op free function; in CopySliceToGo if len(dst)==0 return immediately.
Also change CopySliceToGo to use only len(dst) (not cap(dst)) when calculating
total bytes to copy so you only write to the slice's actual underlying array.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 655c0802-2a60-47ad-ba15-8d44b37e3917

📥 Commits

Reviewing files that changed from the base of the PR and between ec1897a and 3c999a0.

📒 Files selected for processing (5)
  • examples/raygui/main.go
  • index/index.js
  • index/runtime.js
  • raygui/raygui.go
  • wasm-runtime/wasm.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • examples/raygui/main.go

Comment thread raygui/raygui.go
Comment on lines +596 to +603
//go:wasmimport raylib _GuiGetColor
//go:noescape
func guiGetColor(control, property uint16) rl.Color

func GetColor(control ControlID, property PropertyID) rl.Color {
color := guiGetStyle(int32(control), int32(property))
return rl.Color{R: uint8(color >> 24), G: uint8(color >> 16), B: uint8(color >> 8), A: uint8(color)}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Find `//go:wasmimport` function declarations in raygui.go that use known struct types by value.
# Expected after fix: no output.

python - <<'PY'
import pathlib
import re

path = pathlib.Path("raygui/raygui.go")
lines = path.read_text().splitlines()

patterns = [
    re.compile(r'(?<!\*)\brl\.(Rectangle|Color|Vector2|Vector3|Font)\b'),
    re.compile(r'(?<!\*)\bcolor\.RGBA\b'),
]

for i, line in enumerate(lines):
    if "//go:wasmimport" not in line:
        continue
    for j in range(i + 1, min(i + 6, len(lines))):
        candidate = lines[j].strip()
        if not candidate.startswith("func "):
            continue
        if any(pattern.search(candidate) for pattern in patterns):
            print(f"{path}:{j + 1}: {candidate}")
        break
PY

Repository: BrownNPC/Raylib-Go-Wasm

Length of output: 4501


Replace struct-by-value //go:wasmimport signatures with pointer/scalar shims.

The //go:wasmimport imports use Go structs like rl.Color, rl.Rectangle, rl.Vector2, rl.Vector3, and color.RGBA directly in wasm-imported signatures, which violates Go's compiler rules. The Go compiler only permits scalar Wasm types, unsafe.Pointer, restricted pointer types, and string parameters for //go:wasmimport functions. Refactor these signatures using unsafe.Pointer/out-parameter shims or scalar field packing.

This affects 42 imported raygui functions across the file, including the examples at lines 598, 611, 1658, 1724, and 1795 that return or pass rl.Color, rl.Rectangle, color.RGBA, and rl.Vector2 by value.

Reference: https://pkg.go.dev/cmd/compile#hdr-WebAssembly_Directives

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@raygui/raygui.go` around lines 596 - 603, The wasm-imported function
signatures that use struct-by-value types (e.g., guiGetColor returning rl.Color)
must be replaced with pointer/scalar shims: change the //go:wasmimport
declarations (like guiGetColor) to accept an unsafe.Pointer (or individual
scalar fields / packed uint32) as an out-parameter or scalars for each field,
then implement Go wrapper functions (e.g., GetColor) that call the shim and
reconstruct the rl.Color value from the memory/returned scalars; apply the same
pattern for all other imports that currently pass/return rl.Color, rl.Rectangle,
rl.Vector2, rl.Vector3, color.RGBA (create shims named clearly per original like
guiGetRectangle_shim or guiGetColor_shim), use unsafe.Pointer to write/read
struct bytes or pack/unpack fields into uint32/uint64 scalars, and keep the
original exported wrapper signatures (GetColor, etc.) unchanged so callers
continue to receive struct values.

Comment thread raygui/raygui.go
Comment on lines +814 to +875
//go:wasmimport raylib _GuiToggle
//go:noescape
func guiToggle(bounds rl.Rectangle, text wasm.Cptr, active int32) int32

// Toggle control, returns true when active
func Toggle(bounds rl.Rectangle, text string, active bool) bool {
var cbounds rl.Rectangle
cbounds.X = float32(bounds.X)
cbounds.Y = float32(bounds.Y)
cbounds.Width = float32(bounds.Width)
cbounds.Height = float32(bounds.Height)
var ctext wasm.Cptr
if len(text) > 0 {
ctext = wasm.CString(text)
defer wasm.Free(ctext)
}
cactive := wasm.CBool(active)
guiToggle(cbounds, ctext, cactive)
return wasm.GoBool(cactive)
}

//go:wasmimport raylib _GuiToggleGroup
//go:noescape
func guiToggleGroup(bounds rl.Rectangle, text wasm.Cptr, active int32) int32

// ToggleGroup control, returns active toggle index
func ToggleGroup(bounds rl.Rectangle, text string, active int32) int32 {
var cbounds rl.Rectangle
cbounds.X = float32(bounds.X)
cbounds.Y = float32(bounds.Y)
cbounds.Width = float32(bounds.Width)
cbounds.Height = float32(bounds.Height)
var ctext wasm.Cptr
if len(text) > 0 {
ctext = wasm.CString(text)
defer wasm.Free(ctext)
}
cactive := int32(active)
guiToggleGroup(cbounds, ctext, cactive)
return int32(cactive)
}

//go:wasmimport raylib _GuiToggleSlider
//go:noescape
func guiToggleSlider(bounds rl.Rectangle, text wasm.Cptr, active int32) int32

// ToggleSlider control, returns true when clicked
func ToggleSlider(bounds rl.Rectangle, text string, active int32) int32 {
var cbounds rl.Rectangle
cbounds.X = float32(bounds.X)
cbounds.Y = float32(bounds.Y)
cbounds.Width = float32(bounds.Width)
cbounds.Height = float32(bounds.Height)
var ctext wasm.Cptr
if len(text) > 0 {
ctext = wasm.CString(text)
defer wasm.Free(ctext)
}
cactive := int32(active)
guiToggleSlider(cbounds, ctext, cactive)
return int32(cactive)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Inspect wrappers for ignored return values from value-returning raygui controls.
# Expected after fix: wrappers should directly return or assign the result of the imported gui* call.

rg -n -C3 'gui(Toggle|ToggleGroup|ToggleSlider|ComboBox|Slider|SliderBar|ProgressBar|ListView|ColorPanel|ColorBarAlpha|ColorBarHue|ColorPicker)\(' raygui/raygui.go

Repository: BrownNPC/Raylib-Go-Wasm

Length of output: 6006


Return the updated control values from raygui.

These wrappers call value-returning controls but discard the returned value, then return the original active, value, or color. Toggles, combo boxes, sliders, progress bars, list view, and color controls will appear stuck.

Suggested fixes for the affected wrappers
-	guiToggle(cbounds, ctext, cactive)
-	return wasm.GoBool(cactive)
+	return wasm.GoBool(guiToggle(cbounds, ctext, cactive))
@@
-	guiToggleGroup(cbounds, ctext, cactive)
-	return int32(cactive)
+	return guiToggleGroup(cbounds, ctext, cactive)
@@
-	guiToggleSlider(cbounds, ctext, cactive)
-	return int32(cactive)
+	return guiToggleSlider(cbounds, ctext, cactive)
@@
-	guiComboBox(cbounds, ctext, cactive)
-	return int32(cactive)
+	return guiComboBox(cbounds, ctext, cactive)
@@
-	guiSlider(cbounds, ctextLeft, ctextRight, cvalue, cminValue, cmaxValue)
-	return float32(cvalue)
+	return guiSlider(cbounds, ctextLeft, ctextRight, cvalue, cminValue, cmaxValue)
@@
-	guiSliderBar(cbounds, ctextLeft, ctextRight, cvalue, cminValue, cmaxValue)
-	return float32(cvalue)
+	return guiSliderBar(cbounds, ctextLeft, ctextRight, cvalue, cminValue, cmaxValue)
@@
-	guiProgressBar(cbounds, ctextLeft, ctextRight, cvalue, cminValue, cmaxValue)
-	return float32(cvalue)
+	return guiProgressBar(cbounds, ctextLeft, ctextRight, cvalue, cminValue, cmaxValue)
@@
-	guiListView(cbounds, ctext, &cscrollIndex, cactive)
-	return int32(cactive)
+	return guiListView(cbounds, ctext, &cscrollIndex, cactive)
@@
-	guiColorPanel(cbounds, ctext, ccolor)
-	var goRes rl.Color
-	goRes.A = byte(ccolor.A)
-	goRes.R = byte(ccolor.R)
-	goRes.G = byte(ccolor.G)
-	goRes.B = byte(ccolor.B)
-	return goRes
+	return guiColorPanel(cbounds, ctext, ccolor)
@@
-	guiColorBarAlpha(cbounds, ctext, calpha)
-	return float32(calpha)
+	return guiColorBarAlpha(cbounds, ctext, calpha)
@@
-	guiColorBarHue(cbounds, ctext, cvalue)
-	return float32(cvalue)
+	return guiColorBarHue(cbounds, ctext, cvalue)
@@
-	guiColorPicker(cbounds, ctext, ccolor)
-	var goRes rl.Color
-	goRes.A = byte(ccolor.A)
-	goRes.R = byte(ccolor.R)
-	goRes.G = byte(ccolor.G)
-	goRes.B = byte(ccolor.B)
-	return goRes
+	return guiColorPicker(cbounds, ctext, ccolor)

Affects lines 814-875, 897-916, 1088-1179, 1223-1250, 1294-1364, 1366-1395.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@raygui/raygui.go` around lines 814 - 875, The wrappers (Toggle, ToggleGroup,
ToggleSlider) call the native functions (guiToggle, guiToggleGroup,
guiToggleSlider) but ignore their return values and instead return the original
input; capture each native call's return and return that converted value (e.g.
for Toggle use the int32 result from guiToggle and convert with wasm.GoBool
instead of returning the original cactive, for ToggleGroup and ToggleSlider
return the int32 result from guiToggleGroup/guiToggleSlider directly). Update
the code paths referencing wasm.CBool/wasm.GoBool and the int32 cactive
variables in Toggle, ToggleGroup, and ToggleSlider so the functions return the
updated control state from the native call.

Comment thread raygui/raygui.go
Comment on lines +1587 to +1595
//go:wasmimport raylib _GuiSetTooltip
//go:noescape
func guiSetTooltip(tooltip wasm.Cptr)

// Set tooltip string
func SetTooltip(tooltip string) {
ctooltip := wasm.CString(tooltip)
defer wasm.Free(ctooltip)
guiSetTooltip(ctooltip)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Search raygui sources for tooltip storage behavior.
# Expected: if GuiSetTooltip assigns a global tooltip pointer, callers must keep the C string alive.

rg -n -C4 '\bGuiSetTooltip\b|tooltipPtr|guiTooltip'

Repository: BrownNPC/Raylib-Go-Wasm

Length of output: 586


🏁 Script executed:

#!/bin/bash
# Search for tooltipPtr declaration and usage in raygui.go
rg -n 'tooltipPtr|tooltipCptr' raygui/raygui.go

Repository: BrownNPC/Raylib-Go-Wasm

Length of output: 132


🏁 Script executed:

#!/bin/bash
# Look at the full tooltip management section to understand the implementation
sed -n '1564,1620p' raygui/raygui.go

Repository: BrownNPC/Raylib-Go-Wasm

Length of output: 1526


🏁 Script executed:

#!/bin/bash
# Search for GuiSetTooltip in C raygui sources if available
find . -name "*.h" -o -name "*.c" | xargs grep -l "GuiSetTooltip" 2>/dev/null | head -5

Repository: BrownNPC/Raylib-Go-Wasm

Length of output: 49


🏁 Script executed:

#!/bin/bash
# Search for all functions that use wasm.CString with defer wasm.Free pattern
# to identify if this is a broader pattern issue
rg -B2 -A2 'wasm\.CString' raygui/raygui.go | grep -A2 -B2 'defer wasm.Free'

Repository: BrownNPC/Raylib-Go-Wasm

Length of output: 4336


🏁 Script executed:

#!/bin/bash
# Check if there's any documentation or external reference about raygui's GuiSetTooltip
grep -r "GuiSetTooltip" . --include="*.md" --include="*.txt" 2>/dev/null | head -10

Repository: BrownNPC/Raylib-Go-Wasm

Length of output: 49


🌐 Web query:

raygui GuiSetTooltip documentation behavior pointer copy

💡 Result:

GuiSetTooltip in raygui takes a const char* tooltip parameter and sets the tooltip string for subsequent GUI controls. It stores a pointer to the provided string rather than copying it, as is typical for immediate-mode GUI libraries like raygui to avoid allocation overhead. Users must ensure the pointed-to string outlives the tooltip usage or remains valid during the frame, since raygui is immediate-mode with no retained data beyond simple globals. The tooltip functions (GuiEnableTooltip, GuiDisableTooltip, GuiSetTooltip) were marked as REMOVED in version 2.9 (17-Mar-2021) but remain in the header. No examples use tooltips in the test suite, and documentation is limited to the header comments.

Citations:


🏁 Script executed:

#!/bin/bash
# Check raygui version used in this repo
find . -name "go.mod" -o -name "go.sum" | xargs cat 2>/dev/null | grep -i raygui

Repository: BrownNPC/Raylib-Go-Wasm

Length of output: 248


🏁 Script executed:

#!/bin/bash
# Check if SetTooltip or tooltip functions are used/tested anywhere
rg -r 'SetTooltip|EnableTooltip|DisableTooltip' --include="*.go" | grep -v "^raygui/raygui.go"

Repository: BrownNPC/Raylib-Go-Wasm

Length of output: 508


🏁 Script executed:

#!/bin/bash
# Check if there are any comments or version references in raygui.go
head -50 raygui/raygui.go | grep -i "version\|raygui\|copyright"

Repository: BrownNPC/Raylib-Go-Wasm

Length of output: 81


🏁 Script executed:

#!/bin/bash
# Search for SetTooltip usage with correct rg syntax
rg 'SetTooltip|EnableTooltip|DisableTooltip' -t go . --max-count=20

Repository: BrownNPC/Raylib-Go-Wasm

Length of output: 649


🏁 Script executed:

#!/bin/bash
# Check for any test files or examples
find . -name "*test*" -o -name "*example*" | head -20

Repository: BrownNPC/Raylib-Go-Wasm

Length of output: 88


🏁 Script executed:

#!/bin/bash
# Look at raygui.go header to see if there are version comments
head -100 raygui/raygui.go

Repository: BrownNPC/Raylib-Go-Wasm

Length of output: 2025


Fix dangling pointer in SetTooltip by maintaining string lifetime across frames.

The code allocates and immediately frees a C string that raygui stores in a global pointer. The comment at line 1566 confirms: "Tooltips requires some global variables: tooltipPtr". Since GuiSetTooltip stores the pointer rather than copying the string, the defer wasm.Free(ctooltip) creates a dangling pointer for later frames.

Suggested lifetime fix
+var tooltipCptr wasm.Cptr
+
 // Set tooltip string
 func SetTooltip(tooltip string) {
-	ctooltip := wasm.CString(tooltip)
-	defer wasm.Free(ctooltip)
-	guiSetTooltip(ctooltip)
+	var next wasm.Cptr
+	if tooltip != "" {
+		next = wasm.CString(tooltip)
+	}
+	if tooltipCptr != 0 {
+		wasm.Free(tooltipCptr)
+	}
+	tooltipCptr = next
+	guiSetTooltip(tooltipCptr)
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
//go:wasmimport raylib _GuiSetTooltip
//go:noescape
func guiSetTooltip(tooltip wasm.Cptr)
// Set tooltip string
func SetTooltip(tooltip string) {
ctooltip := wasm.CString(tooltip)
defer wasm.Free(ctooltip)
guiSetTooltip(ctooltip)
//go:wasmimport raylib _GuiSetTooltip
//go:noescape
func guiSetTooltip(tooltip wasm.Cptr)
var tooltipCptr wasm.Cptr
// Set tooltip string
func SetTooltip(tooltip string) {
var next wasm.Cptr
if tooltip != "" {
next = wasm.CString(tooltip)
}
if tooltipCptr != 0 {
wasm.Free(tooltipCptr)
}
tooltipCptr = next
guiSetTooltip(tooltipCptr)
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@raygui/raygui.go` around lines 1587 - 1595, SetTooltip currently allocates a
C string, calls guiSetTooltip, then immediately frees it, leaving raygui with a
dangling pointer; instead keep the C string alive by storing it in a
package-level variable (e.g., tooltipPtr) that persists across frames. Change
SetTooltip to: allocate ctooltip with wasm.CString, free any previous tooltipPtr
if non-nil, assign tooltipPtr = ctooltip, and then call
guiSetTooltip(tooltipPtr); ensure guiSetTooltip and tooltipPtr are the
referenced symbols used so the pointer lifetime is maintained until explicitly
replaced or freed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants