diff --git a/Benchmarks/benchmark_common.hpp b/Benchmarks/benchmark_common.hpp index 122c0746..7b9e8136 100644 --- a/Benchmarks/benchmark_common.hpp +++ b/Benchmarks/benchmark_common.hpp @@ -168,6 +168,37 @@ struct SharedObject : std::enable_shared_from_this } }; +struct Vec3Source +{ + float x = 0.f, y = 0.f, z = 0.f; + Vec3Source() = default; + Vec3Source(float x_, float y_, float z_) : x(x_), y(y_), z(z_) {} +}; + +struct Vec3Target +{ + float x = 0.f, y = 0.f, z = 0.f; + Vec3Target() = default; + Vec3Target(float x_, float y_, float z_) : x(x_), y(y_), z(z_) {} +}; + +struct ColorSource +{ + float r = 0.f, g = 0.f, b = 0.f; + ColorSource() = default; + ColorSource(float r_, float g_, float b_) : r(r_), g(g_), b(b_) {} +}; + +inline float sumVec3(Vec3Target v) +{ + return v.x + v.y + v.z; +} + +inline float sumVec3Ref(const Vec3Target& v) +{ + return v.x + v.y + v.z; +} + inline Basic* basic_return() { static Basic value{}; diff --git a/Benchmarks/benchmark_luabridge.cpp b/Benchmarks/benchmark_luabridge.cpp index 9dad4345..8eff0c0d 100644 --- a/Benchmarks/benchmark_luabridge.cpp +++ b/Benchmarks/benchmark_luabridge.cpp @@ -267,7 +267,7 @@ void multi_return_measure(benchmark::State& state) setSkipped(state, "unsupported conceptual multi-return conversion in LuaBridge vanilla"); } -void base_derived_measure(benchmark::State& state) +void derived_base_measure(benchmark::State& state) { setSkipped(state, "unsupported for multi inheritance in LuaBridge vanilla"); } @@ -498,7 +498,7 @@ BENCHMARK(userdata_variable_access_last_measure)->Name("userdata_variable_access BENCHMARK(multi_return_lua_measure)->Name("multi_return_lua_measure"); BENCHMARK(multi_return_measure)->Name("multi_return_measure"); BENCHMARK(stateful_function_object_measure)->Name("stateful_function_object_measure"); -BENCHMARK(base_derived_measure)->Name("base_derived_measure"); +BENCHMARK(derived_base_measure)->Name("derived_base_measure"); BENCHMARK(return_userdata_measure)->Name("return_userdata_measure"); BENCHMARK(optional_success_measure)->Name("optional_success_measure"); BENCHMARK(optional_half_failure_measure)->Name("optional_half_failure_measure"); diff --git a/Benchmarks/benchmark_luabridge3.cpp b/Benchmarks/benchmark_luabridge3.cpp index dc2f8225..83659702 100644 --- a/Benchmarks/benchmark_luabridge3.cpp +++ b/Benchmarks/benchmark_luabridge3.cpp @@ -12,6 +12,34 @@ #include #include +namespace luabridge { + +template <> +struct StackConversion +{ + static constexpr bool enabled = true; +}; + +template <> +struct StackConverter +{ + static lbsbench::Vec3Target convert(const lbsbench::Vec3Source& s) + { + return {s.x, s.y, s.z}; + } +}; + +template <> +struct StackConverter +{ + static lbsbench::Vec3Target convert(const lbsbench::ColorSource& s) + { + return {s.r, s.g, s.b}; + } +}; + +} // namespace luabridge + namespace { using namespace lbsbench; @@ -396,7 +424,7 @@ void multi_return_measure(benchmark::State& state) benchmark::DoNotOptimize(x); } -void base_derived_measure(benchmark::State& state) +void derived_base_measure(benchmark::State& state) { lua_State* L = makeLua(); @@ -657,6 +685,88 @@ void implicit_inheritance_measure(benchmark::State& state) } } +void registerConverter(lua_State* L) +{ + luabridge::getGlobalNamespace(L) + .beginClass("Vec3Source") + .addConstructor() + .addConverter() + .endClass() + .beginClass("ColorSource") + .addConstructor() + .addConverter() + .endClass() + .beginClass("Vec3Target") + .addConstructor() + .endClass() + .addFunction("sumVec3", &sumVec3) + .addFunction("sumVec3Ref", &sumVec3Ref); +} + +void converter_exact_type_measure(benchmark::State& state) +{ + lua_State* L = makeLua(); + registerConverter(L); + luaDoStringOrThrow(L, "obj = Vec3Target(1, 2, 3)", "converter_exact_type setup"); + luaDoStringOrThrow(L, "function invoke_exact() return sumVec3(obj) end", "converter_exact_type closure setup"); + + for (auto _ : state) + { + (void) _; + lua_getglobal(L, "invoke_exact"); + luaCheckOrThrow(L, lua_pcall(L, 0, 1, 0), "invoke_exact"); + lua_pop(L, 1); + } +} + +void converter_phase3_value_measure(benchmark::State& state) +{ + lua_State* L = makeLua(); + registerConverter(L); + luaDoStringOrThrow(L, "obj = Vec3Source(1, 2, 3)", "converter_phase3_value setup"); + luaDoStringOrThrow(L, "function invoke_conv_value() return sumVec3(obj) end", "converter_phase3_value closure setup"); + + for (auto _ : state) + { + (void) _; + lua_getglobal(L, "invoke_conv_value"); + luaCheckOrThrow(L, lua_pcall(L, 0, 1, 0), "invoke_conv_value"); + lua_pop(L, 1); + } +} + +void converter_phase3_ref_measure(benchmark::State& state) +{ + lua_State* L = makeLua(); + registerConverter(L); + luaDoStringOrThrow(L, "obj = Vec3Source(1, 2, 3)", "converter_phase3_ref setup"); + luaDoStringOrThrow(L, "function invoke_conv_ref() return sumVec3Ref(obj) end", "converter_phase3_ref closure setup"); + + for (auto _ : state) + { + (void) _; + lua_getglobal(L, "invoke_conv_ref"); + luaCheckOrThrow(L, lua_pcall(L, 0, 1, 0), "invoke_conv_ref"); + lua_pop(L, 1); + } +} + +void converter_multi_registered_measure(benchmark::State& state) +{ + lua_State* L = makeLua(); + registerConverter(L); + luaDoStringOrThrow(L, "obj = ColorSource(0.5, 1, 0)", "converter_multi_registered setup"); + luaDoStringOrThrow(L, "function invoke_conv_multi() return sumVec3(obj) end", "converter_multi_registered closure setup"); + + for (auto _ : state) + { + (void) _; + lua_getglobal(L, "invoke_conv_multi"); + luaCheckOrThrow(L, lua_pcall(L, 0, 1, 0), "invoke_conv_multi"); + lua_pop(L, 1); + } +} + } // namespace BENCHMARK(table_global_string_get_measure)->Name("table_global_string_get_measure"); @@ -675,7 +785,7 @@ BENCHMARK(userdata_variable_access_last_measure)->Name("userdata_variable_access BENCHMARK(multi_return_lua_measure)->Name("multi_return_lua_measure"); BENCHMARK(multi_return_measure)->Name("multi_return_measure"); BENCHMARK(stateful_function_object_measure)->Name("stateful_function_object_measure"); -BENCHMARK(base_derived_measure)->Name("base_derived_measure"); +BENCHMARK(derived_base_measure)->Name("derived_base_measure"); BENCHMARK(return_userdata_measure)->Name("return_userdata_measure"); BENCHMARK(optional_success_measure)->Name("optional_success_measure"); BENCHMARK(optional_half_failure_measure)->Name("optional_half_failure_measure"); @@ -689,3 +799,7 @@ BENCHMARK(shared_ptr_return_measure)->Name("shared_ptr_return_measure"); BENCHMARK(shared_ptr_pass_measure)->Name("shared_ptr_pass_measure"); BENCHMARK(static_member_function_call_measure)->Name("static_member_function_call_measure"); BENCHMARK(derived_method_call_measure)->Name("derived_method_call_measure"); +BENCHMARK(converter_exact_type_measure)->Name("converter_exact_type_measure"); +BENCHMARK(converter_phase3_value_measure)->Name("converter_phase3_value_measure"); +BENCHMARK(converter_phase3_ref_measure)->Name("converter_phase3_ref_measure"); +BENCHMARK(converter_multi_registered_measure)->Name("converter_multi_registered_measure"); diff --git a/Benchmarks/benchmark_sol3.cpp b/Benchmarks/benchmark_sol3.cpp index 44e9e562..17c06bb6 100644 --- a/Benchmarks/benchmark_sol3.cpp +++ b/Benchmarks/benchmark_sol3.cpp @@ -348,7 +348,7 @@ void multi_return_measure(benchmark::State& state) benchmark::DoNotOptimize(x); } -void base_derived_measure(benchmark::State& state) +void derived_base_measure(benchmark::State& state) { sol::state lua; @@ -602,7 +602,7 @@ BENCHMARK(userdata_variable_access_last_measure)->Name("userdata_variable_access BENCHMARK(multi_return_lua_measure)->Name("multi_return_lua_measure"); BENCHMARK(multi_return_measure)->Name("multi_return_measure"); BENCHMARK(stateful_function_object_measure)->Name("stateful_function_object_measure"); -BENCHMARK(base_derived_measure)->Name("base_derived_measure"); +BENCHMARK(derived_base_measure)->Name("derived_base_measure"); BENCHMARK(return_userdata_measure)->Name("return_userdata_measure"); BENCHMARK(optional_success_measure)->Name("optional_success_measure"); BENCHMARK(optional_half_failure_measure)->Name("optional_half_failure_measure"); diff --git a/Benchmarks/plot_benchmarks.py b/Benchmarks/plot_benchmarks.py index c8880604..dc85157d 100644 --- a/Benchmarks/plot_benchmarks.py +++ b/Benchmarks/plot_benchmarks.py @@ -4,9 +4,8 @@ import argparse import json -import math -import os from collections import defaultdict +from pathlib import Path import matplotlib.pyplot as plt import matplotlib.patches as mpatches @@ -18,7 +17,6 @@ _SUFFIX = "_measure" def _clean_label(name: str) -> str: - """Convert a raw benchmark name to a human-readable label.""" if name.endswith(_SUFFIX): name = name[: -len(_SUFFIX)] return name.replace("_", " ") @@ -27,8 +25,7 @@ def _clean_label(name: str) -> str: # ── JSON loading ────────────────────────────────────────────────────────────── def infer_library_name(path: str) -> str: - stem = os.path.splitext(os.path.basename(path))[0] - return stem.replace("benchmark_", "") + return Path(path).stem.replace("benchmark_", "") def load_google_benchmark_json(path: str, library_name: str) -> dict: @@ -36,16 +33,22 @@ def load_google_benchmark_json(path: str, library_name: str) -> dict: data = json.load(f) case_values: dict[str, float] = {} + case_stddev: dict[str, float] = {} case_errors: dict[str, str] = {} for entry in data.get("benchmarks", []): name = entry.get("name", "") run_type = entry.get("run_type", "") - # Prefer aggregate mean when available - if run_type == "aggregate" and entry.get("aggregate_name") == "mean": + # Prefer aggregate mean/stddev when available + if run_type == "aggregate": base_name = entry.get("run_name", name) - case_values[base_name] = entry.get("real_time", entry.get("cpu_time", 0.0)) + agg = entry.get("aggregate_name") + t = entry.get("real_time", entry.get("cpu_time", 0.0)) + if agg == "mean": + case_values[base_name] = t + elif agg == "stddev": + case_stddev[base_name] = t continue if run_type not in ("iteration", ""): @@ -58,33 +61,36 @@ def load_google_benchmark_json(path: str, library_name: str) -> dict: if name not in case_values: case_values[name] = entry.get("real_time", entry.get("cpu_time", 0.0)) - return {"library": library_name, "values": case_values, "errors": case_errors} + return {"library": library_name, "values": case_values, "stddev": case_stddev, "errors": case_errors} # ── Merge ───────────────────────────────────────────────────────────────────── def merge_results(result_sets): merged: dict[str, dict[str, float]] = defaultdict(dict) + stddev: dict[str, dict[str, float]] = defaultdict(dict) errors: dict[str, dict[str, str]] = defaultdict(dict) for result in result_sets: lib = result["library"] for case_name, value in result["values"].items(): merged[case_name][lib] = value + for case_name, sd in result.get("stddev", {}).items(): + stddev[case_name][lib] = sd for case_name, error in result["errors"].items(): errors[case_name][lib] = error - return merged, errors + return merged, stddev, errors # ── Plotting ────────────────────────────────────────────────────────────────── # Dark theme colours -_BG = "#1E1E2E" # figure / axes background -_FG = "#CDD6F4" # text, ticks, labels -_GRID = "#313244" # grid lines -_SPINE = "#45475A" # axis spines -_UNSUP = "#585B70" # "unsupported" text +_BG = "#1E1E2E" # figure / axes background +_FG = "#CDD6F4" # text, ticks, labels +_GRID = "#313244" # grid lines +_SPINE = "#45475A" # axis spines +_UNSUP = "#585B70" # "unsupported" text # Bright palette suited for dark backgrounds _PALETTE = [ @@ -98,123 +104,140 @@ def merge_results(result_sets): "#89DCEB", # sky ] +_LIB_ORDER = ["LuaBridge3Benchmark", "LuaBridgeVanillaBenchmark", "Sol3Benchmark"] +_LIB_ORDER_MAP = {lib: i for i, lib in enumerate(_LIB_ORDER)} -def plot_grouped_bars(merged: dict, errors: dict, output_file: str, log_scale: bool = False) -> None: + +def plot_grouped_bars(merged: dict, stddev: dict, errors: dict, output_file: str, log_scale: bool = False) -> None: case_names = sorted(merged.keys()) - libraries = sorted({lib for cases in merged.values() for lib in cases}) + all_libs = {lib for cases in merged.values() for lib in cases} + libraries = sorted(all_libs, key=lambda l: _LIB_ORDER_MAP.get(l, len(_LIB_ORDER))) if not case_names or not libraries: raise RuntimeError("No benchmark samples found to plot") n_cases = len(case_names) n_libs = len(libraries) + clean_labels = [_clean_label(cn) for cn in case_names] # ── Layout ──────────────────────────────────────────────────────────────── bar_h = 0.80 group_h = bar_h / n_libs fig_h = max(10, n_cases * bar_h + 2) - plt.rcParams.update({ - "text.color": _FG, - "axes.labelcolor": _FG, - "xtick.color": _FG, - "ytick.color": _FG, - }) - - fig, ax = plt.subplots(figsize=(12, fig_h)) - fig.patch.set_facecolor(_BG) - ax.set_facecolor(_BG) - - colors = {lib: _PALETTE[i % len(_PALETTE)] for i, lib in enumerate(libraries)} - - y_positions = np.arange(n_cases, dtype=float) - - for i, library in enumerate(libraries): - values = [merged[cn].get(library, float("nan")) for cn in case_names] - offsets = (i - (n_libs - 1) / 2.0) * group_h - bar_y = y_positions + offsets - ax.barh( - bar_y, - values, - height=group_h * 0.85, - color=colors[library], - label=library, - zorder=4, + with plt.rc_context({ + "text.color": _FG, + "axes.labelcolor": _FG, + "xtick.color": _FG, + "ytick.color": _FG, + }): + fig, ax = plt.subplots(figsize=(14, fig_h)) + fig.patch.set_facecolor(_BG) + ax.set_facecolor(_BG) + + colors = {lib: _PALETTE[i % len(_PALETTE)] for i, lib in enumerate(libraries)} + y_positions = np.arange(n_cases, dtype=float) + + # Max value including error bars — used to size x-axis + x_max_with_err = 1.0 + for cn in case_names: + for lib in libraries: + val = merged[cn].get(lib, float("nan")) + if not np.isnan(val): + sd = stddev.get(cn, {}).get(lib, 0.0) or 0.0 + x_max_with_err = max(x_max_with_err, val + sd) + + for i, library in enumerate(libraries): + values = [merged[cn].get(library, float("nan")) for cn in case_names] + sds = [stddev.get(cn, {}).get(library, float("nan")) for cn in case_names] + bar_y = y_positions + (i - (n_libs - 1) / 2.0) * group_h + + xerr_vals = [sd if not np.isnan(sd) else 0.0 for sd in sds] + has_errors = any(sd > 0 for sd in xerr_vals) + + ax.barh( + bar_y, + values, + height=group_h * 0.85, + color=colors[library], + label=library, + xerr=xerr_vals if has_errors else None, + error_kw={"ecolor": _FG, "capsize": 3, "elinewidth": 1.2, "capthick": 1.2}, + zorder=4, + ) + + for y, val, sd in zip(bar_y, values, sds): + if np.isnan(val): + ax.text( + 0, y, " unsupported", + va="center", ha="left", + fontsize=10, color=_UNSUP, style="italic", + zorder=5, + ) + else: + label = f" {val:.1f} ±{sd:.1f} ns" if not np.isnan(sd) and sd > 0 else f" {val:.1f} ns" + ax.text( + val, y, label, + va="center", ha="left", + fontsize=9, color=_FG, + zorder=5, + ) + + # ── Axes ────────────────────────────────────────────────────────────── + ax.set_yticks(y_positions) + ax.set_yticklabels(clean_labels, fontsize=12) + + half_span = (n_libs - 1) / 2.0 * group_h + group_h * 0.425 + ax.set_ylim(-half_span, n_cases - 1 + half_span) + ax.invert_yaxis() + + if log_scale: + ax.set_xscale("log") + ax.set_xlabel("Time (ns, log scale)", fontsize=13) + else: + ax.set_xlabel("Time (ns)", fontsize=13) + + ax.set_xlim(0, x_max_with_err * 1.20) + ax.xaxis.grid(True, color=_GRID, linestyle="--", alpha=1.0, zorder=2) + ax.set_axisbelow(True) + for spine in ax.spines.values(): + spine.set_edgecolor(_SPINE) + ax.spines["top"].set_visible(True) + ax.spines["right"].set_visible(False) + ax.xaxis.set_tick_params(which="both", top=True, bottom=True, labeltop=True, labelbottom=True) + ax.tick_params(axis="x", which="both", color=_SPINE, labelsize=11) + + # ── Legend & title ──────────────────────────────────────────────────── + legend_handles = [mpatches.Patch(color=colors[lib], label=lib) for lib in libraries] + ax.legend( + handles=legend_handles, + loc="upper right", + fontsize=11, + framealpha=1.0, + facecolor=_SPINE, + edgecolor=_SPINE, + labelcolor=_FG, + ) + ax.set_title( + "Lua Binding Benchmarks — lower is better (ns)", + fontsize=16, pad=10, fontweight="bold", color=_FG, ) - # "unsupported" label where no value exists - for y, val in zip(bar_y, values): - if math.isnan(val): - ax.text( - 0, y, - " unsupported", - va="center", ha="left", - fontsize=7, color=_UNSUP, style="italic", - zorder=5, - ) - - # ── Axes ────────────────────────────────────────────────────────────────── - ax.set_yticks(y_positions) - ax.set_yticklabels([_clean_label(cn) for cn in case_names], fontsize=9) - ax.invert_yaxis() - - if log_scale: - ax.set_xscale("log") - ax.set_xlabel("Time (ns, log scale)", fontsize=10) - else: - ax.set_xlabel("Time (ns)", fontsize=10) - - ax.xaxis.grid(True, color=_GRID, linestyle="--", alpha=1.0, zorder=2) - ax.set_axisbelow(True) - for spine in ax.spines.values(): - spine.set_edgecolor(_SPINE) - ax.spines["top"].set_visible(True) - ax.spines["right"].set_visible(False) - - # Tick marks and labels on both top and bottom x-axis - ax.xaxis.set_tick_params(which="both", top=True, bottom=True, labeltop=True, labelbottom=True) - ax.tick_params(axis="x", which="both", color=_SPINE) - - # ── Legend & title ──────────────────────────────────────────────────────── - legend_handles = [ - mpatches.Patch(color=colors[lib], label=lib) for lib in libraries - ] - legend = ax.legend( - handles=legend_handles, - loc="upper right", - fontsize=9, - framealpha=1.0, - facecolor=_SPINE, - edgecolor=_SPINE, - labelcolor=_FG, - ) - ax.set_title( - "Lua Binding Benchmarks — lower is better", - fontsize=13, pad=14, fontweight="bold", color=_FG, - ) - - fig.subplots_adjust(left=0.22, right=0.97, top=0.95, bottom=0.03) - plt.savefig(output_file, dpi=150, facecolor=fig.get_facecolor()) - plt.close() - - plt.rcParams.update({ - "text.color": "black", - "axes.labelcolor": "black", - "xtick.color": "black", - "ytick.color": "black", - }) + fig.subplots_adjust(left=0.22, right=0.97, top=0.97, bottom=0.03) + plt.savefig(output_file, dpi=150, facecolor=fig.get_facecolor()) + plt.close() # ── Text summary ────────────────────────────────────────────────────────── - txt_file = os.path.splitext(output_file)[0] + ".txt" + txt_file = Path(output_file).with_suffix(".txt") col_w = max(len(lib) for lib in libraries) + 2 - label_w = max(len(_clean_label(cn)) for cn in case_names) + 2 + label_w = max(len(lbl) for lbl in clean_labels) + 2 with open(txt_file, "w", encoding="utf-8") as f: header = f"{'Benchmark':<{label_w}}" + "".join(f"{lib:>{col_w}}" for lib in libraries) f.write(header + "\n") f.write("-" * len(header) + "\n") - for cn in case_names: - row = f"{_clean_label(cn):<{label_w}}" + for cn, lbl in zip(case_names, clean_labels): + row = f"{lbl:<{label_w}}" for lib in libraries: val = merged[cn].get(lib) cell = f"{val:>{col_w - 3}.1f} ns" if val is not None else f"{'n/a':>{col_w}}" @@ -248,13 +271,10 @@ def main(): for path in args.input ] - merged, errors = merge_results(result_sets) - - output_dir = os.path.dirname(args.output) - if output_dir: - os.makedirs(output_dir, exist_ok=True) + merged, stddev, errors = merge_results(result_sets) - plot_grouped_bars(merged, errors, args.output, log_scale=args.log) + Path(args.output).parent.mkdir(parents=True, exist_ok=True) + plot_grouped_bars(merged, stddev, errors, args.output, log_scale=args.log) print(f"Saved: {args.output}") diff --git a/Distribution/LuaBridge/LuaBridge.h b/Distribution/LuaBridge/LuaBridge.h index f7d0b543..89e3215c 100644 --- a/Distribution/LuaBridge/LuaBridge.h +++ b/Distribution/LuaBridge/LuaBridge.h @@ -48,28 +48,28 @@ #include #endif -#if defined(__has_include) && __has_include() && (__cplusplus >= 202002L || (defined(_MSVC_LANG) && _MSVC_LANG >= 202002L)) -#include -#endif - -#if defined(__has_include) && __has_include() && (__cplusplus >= 202302L || (defined(_MSVC_LANG) && _MSVC_LANG >= 202302L)) -#include +#if defined(__has_include) && __has_include() && (__cplusplus >= 202002L || (defined(_MSVC_LANG) && _MSVC_LANG >= 202002L)) +#include #endif -#if defined(__has_include) && __has_include() && (__cplusplus >= 202002L || (defined(_MSVC_LANG) && _MSVC_LANG >= 202002L)) -#include +#if defined(__has_include) && __has_include() && (__cplusplus >= 202002L || (defined(_MSVC_LANG) && _MSVC_LANG >= 202002L)) +#include #endif -#if defined(__has_include) && __has_include() && (__cplusplus >= 202002L || (defined(_MSVC_LANG) && _MSVC_LANG >= 202002L)) -#include +#if defined(__has_include) && __has_include() && (__cplusplus >= 202302L || (defined(_MSVC_LANG) && _MSVC_LANG >= 202302L)) +#include #endif #if defined(__has_include) && __has_include() && (__cplusplus >= 202302L || (defined(_MSVC_LANG) && _MSVC_LANG >= 202302L)) #include #endif -#if defined(__has_include) && __has_include() && (__cplusplus >= 202302L || (defined(_MSVC_LANG) && _MSVC_LANG >= 202302L)) -#include +#if defined(__has_include) && __has_include() && (__cplusplus >= 202302L || (defined(_MSVC_LANG) && _MSVC_LANG >= 202302L)) +#include +#endif + +#if defined(__has_include) && __has_include() && (__cplusplus >= 202002L || (defined(_MSVC_LANG) && _MSVC_LANG >= 202002L)) +#include #endif @@ -3644,6 +3644,11 @@ template ().find_first_of('.')> return reinterpret_cast(0x8108); } +[[nodiscard]] inline const void* getConvertersKey() noexcept +{ + return reinterpret_cast(0xc0de); +} + [[nodiscard]] inline const void* getStaticIndexFallbackKey() { return reinterpret_cast(0x81cc); @@ -4846,6 +4851,149 @@ struct StackOpSelector // End File: Source/LuaBridge/detail/Userdata.h +// Begin File: Source/LuaBridge/detail/Converter.h + +namespace luabridge { + +template +struct Stack; + +template +struct StackConversion +{ + static constexpr bool enabled = false; +}; + +template +struct StackConverter; + +namespace detail { + +struct ConverterRegistry +{ + std::unordered_map converters; +}; + +inline ConverterRegistry* getOrCreateConverterRegistry(lua_State* L, int metatableIdx) +{ + const int absIdx = lua_absindex(L, metatableIdx); + + lua_rawgetp_x(L, absIdx, getConvertersKey()); + if (lua_isuserdata(L, -1) && !lua_islightuserdata(L, -1)) + { + auto* reg = align(lua_touserdata(L, -1)); + lua_pop(L, 1); + return reg; + } + lua_pop(L, 1); + + lua_newuserdata_aligned(L); + auto* reg = align(lua_touserdata(L, -1)); + + lua_pushvalue(L, -1); + lua_rawsetp_x(L, absIdx, getConvertersKey()); + lua_pop(L, 1); + + return reg; +} + +template +class ConverterConstRef +{ +public: + explicit ConverterConstRef(const T& value) noexcept + : m_ref(std::addressof(value)) + { + } + + explicit ConverterConstRef(T&& value) noexcept(std::is_nothrow_move_constructible_v) + : m_value(std::move(value)) + , m_ref(std::addressof(*m_value)) + { + } + + ConverterConstRef(ConverterConstRef&& other) noexcept(std::is_nothrow_move_constructible_v) + : m_value(std::move(other.m_value)) + , m_ref(m_value.has_value() ? std::addressof(*m_value) : other.m_ref) + { + } + + ConverterConstRef(const ConverterConstRef& other) + : m_value(other.m_value) + , m_ref(m_value.has_value() ? std::addressof(*m_value) : other.m_ref) + { + } + + ConverterConstRef& operator=(ConverterConstRef&& other) noexcept(std::is_nothrow_move_assignable_v) + { + m_value = std::move(other.m_value); + m_ref = m_value.has_value() ? std::addressof(*m_value) : other.m_ref; + return *this; + } + + ConverterConstRef& operator=(const ConverterConstRef& other) + { + m_value = other.m_value; + m_ref = m_value.has_value() ? std::addressof(*m_value) : other.m_ref; + return *this; + } + + operator const T&() const noexcept + { + return *m_ref; + } + +private: + std::optional m_value; + const T* m_ref = nullptr; +}; + +template +TypeResult tryConvertFromRegisteredConverter(lua_State* L, int index) +{ + using FnType = TypeResult(*)(lua_State*, int); + + if (! lua_getmetatable(L, index)) + return makeErrorCode(ErrorCode::InvalidTypeCast); + + lua_rawgetp_x(L, -1, detail::getConvertersKey()); + if (lua_isuserdata(L, -1) && !lua_islightuserdata(L, -1)) + { + auto* reg = align(lua_touserdata(L, -1)); + lua_pop(L, 2); + + auto it = reg->converters.find(detail::getClassRegistryKey()); + if (it != reg->converters.end() && it->second) + { + const auto* fn = static_cast(it->second); + return (*fn)(L, index); + } + } + else + { + lua_pop(L, 2); + } + + return makeErrorCode(ErrorCode::InvalidTypeCast); +} + +template +TypeResult convertFromStack(lua_State* L, int index) +{ + auto result = detail::Userdata::get(L, index, true); + + if (!result || !*result) + return makeErrorCode(ErrorCode::InvalidTypeCast); + + return StackConverter::convert(**result); +} + +} +} + + +// End File: Source/LuaBridge/detail/Converter.h + // Begin File: Source/LuaBridge/detail/Stack.h #if LUABRIDGE_HAS_CXX17_FILESYSTEM @@ -6149,14 +6297,50 @@ struct Stack } }; +#if LUABRIDGE_HAS_CXX17_FILESYSTEM + +template <> +struct Stack +{ + [[nodiscard]] static Result push(lua_State* L, const std::filesystem::path& path) + { +#if LUABRIDGE_SAFE_STACK_CHECKS + if (! lua_checkstack(L, 1)) + return makeErrorCode(ErrorCode::LuaStackOverflow); +#endif + + lua_pushlstring(L, path.string().c_str(), path.string().size()); + return {}; + } + + [[nodiscard]] static TypeResult get(lua_State* L, int index) + { + if (lua_type(L, index) != LUA_TSTRING) + return makeErrorCode(ErrorCode::InvalidTypeCast); + + std::size_t len = 0; + const char* str = lua_tolstring(L, index, &len); + return std::filesystem::path(std::string(str, len)); + } + + [[nodiscard]] static bool isInstance(lua_State* L, int index) + { + return lua_type(L, index) == LUA_TSTRING; + } +}; +#endif + namespace detail { template -struct StackOpSelector +struct StackOpSelector { using ReturnType = TypeResult; - static Result push(lua_State* L, T& value) { return Stack::push(L, value); } + static Result push(lua_State* L, T* value) + { + return value ? Stack::push(L, *value) : Stack::push(L, nullptr); + } static ReturnType get(lua_State* L, int index) { return Stack::get(L, index); } @@ -6164,11 +6348,14 @@ struct StackOpSelector }; template -struct StackOpSelector +struct StackOpSelector { using ReturnType = TypeResult; - static Result push(lua_State* L, const T& value) { return Stack::push(L, value); } + static Result push(lua_State* L, const T* value) + { + return value ? Stack::push(L, *value) : Stack::push(L, nullptr); + } static ReturnType get(lua_State* L, int index) { return Stack::get(L, index); } @@ -6176,14 +6363,11 @@ struct StackOpSelector }; template -struct StackOpSelector +struct StackOpSelector { using ReturnType = TypeResult; - static Result push(lua_State* L, T* value) - { - return value ? Stack::push(L, *value) : Stack::push(L, nullptr); - } + static Result push(lua_State* L, T& value) { return Stack::push(L, value); } static ReturnType get(lua_State* L, int index) { return Stack::get(L, index); } @@ -6191,14 +6375,11 @@ struct StackOpSelector }; template -struct StackOpSelector +struct StackOpSelector { using ReturnType = TypeResult; - static Result push(lua_State* L, const T* value) - { - return value ? Stack::push(L, *value) : Stack::push(L, nullptr); - } + static Result push(lua_State* L, const T& value) { return Stack::push(L, value); } static ReturnType get(lua_State* L, int index) { return Stack::get(L, index); } @@ -6208,25 +6389,25 @@ struct StackOpSelector } template -struct Stack>> +struct Stack { - using Helper = detail::StackOpSelector::value>; + using Helper = detail::StackOpSelector::value>; using ReturnType = typename Helper::ReturnType; - [[nodiscard]] static Result push(lua_State* L, T& value) { return Helper::push(L, value); } + [[nodiscard]] static Result push(lua_State* L, T* value) { return Helper::push(L, value); } [[nodiscard]] static ReturnType get(lua_State* L, int index) { return Helper::get(L, index); } [[nodiscard]] static bool isInstance(lua_State* L, int index) { return Helper::isInstance(L, index); } }; -template -struct Stack>> +template +struct Stack { - using Helper = detail::StackOpSelector::value>; + using Helper = detail::StackOpSelector::value>; using ReturnType = typename Helper::ReturnType; - [[nodiscard]] static Result push(lua_State* L, const T& value) { return Helper::push(L, value); } + [[nodiscard]] static Result push(lua_State* L, const T* value) { return Helper::push(L, value); } [[nodiscard]] static ReturnType get(lua_State* L, int index) { return Helper::get(L, index); } @@ -6234,63 +6415,105 @@ struct Stack>> }; template -struct Stack +struct Stack && !std::is_const_v>> { - using Helper = detail::StackOpSelector::value>; + using Helper = detail::StackOpSelector::value>; using ReturnType = typename Helper::ReturnType; - [[nodiscard]] static Result push(lua_State* L, T* value) { return Helper::push(L, value); } + [[nodiscard]] static Result push(lua_State* L, T& value) { return Helper::push(L, value); } [[nodiscard]] static ReturnType get(lua_State* L, int index) { return Helper::get(L, index); } [[nodiscard]] static bool isInstance(lua_State* L, int index) { return Helper::isInstance(L, index); } }; -template -struct Stack +template +struct Stack && !StackConversion::enabled>> { - using Helper = detail::StackOpSelector::value>; + using Helper = detail::StackOpSelector::value>; using ReturnType = typename Helper::ReturnType; - [[nodiscard]] static Result push(lua_State* L, const T* value) { return Helper::push(L, value); } + [[nodiscard]] static Result push(lua_State* L, const T& value) { return Helper::push(L, value); } [[nodiscard]] static ReturnType get(lua_State* L, int index) { return Helper::get(L, index); } [[nodiscard]] static bool isInstance(lua_State* L, int index) { return Helper::isInstance(L, index); } }; -#if LUABRIDGE_HAS_CXX17_FILESYSTEM +template +struct Stack::enabled && !std::is_array_v>> +{ + using ReturnType = TypeResult>; -template <> -struct Stack + [[nodiscard]] static Result push(lua_State* L, const T& value) + { + return Stack::push(L, value); + } + + [[nodiscard]] static ReturnType get(lua_State* L, int index) + { + if (detail::Userdata::isInstance(L, index)) + { + auto result = detail::Userdata::get(L, index, true); + if (!result) + return result.error(); + + if (T* ptr = *result) + return detail::ConverterConstRef(*ptr); + + return detail::getNilBadArgError(L, index); + } + + auto converted = detail::tryConvertFromRegisteredConverter(L, index); + if (!converted) + return converted.error(); + + return detail::ConverterConstRef(std::move(*converted)); + } + + [[nodiscard]] static bool isInstance(lua_State* L, int index) + { + return Stack::isInstance(L, index); + } +}; + +template +struct Stack::enabled>> { - [[nodiscard]] static Result push(lua_State* L, const std::filesystem::path& path) + using IsUserdata = void; + using ReturnType = TypeResult; + + [[nodiscard]] static Result push(lua_State* L, const T& value) { -#if LUABRIDGE_SAFE_STACK_CHECKS - if (! lua_checkstack(L, 1)) - return makeErrorCode(ErrorCode::LuaStackOverflow); -#endif + return detail::StackHelper::value>::push(L, value); + } - lua_pushlstring(L, path.string().c_str(), path.string().size()); - return {}; + [[nodiscard]] static Result push(lua_State* L, T&& value) + { + return detail::StackHelper::value>::push(L, std::move(value)); } - [[nodiscard]] static TypeResult get(lua_State* L, int index) + [[nodiscard]] static ReturnType get(lua_State* L, int index) { - if (lua_type(L, index) != LUA_TSTRING) - return makeErrorCode(ErrorCode::InvalidTypeCast); + if (detail::Userdata::isInstance(L, index)) + { + auto result = detail::Userdata::get(L, index, true); + if (result) + { + if (T* ptr = *result) + return *ptr; + } + return result.error(); + } - std::size_t len = 0; - const char* str = lua_tolstring(L, index, &len); - return std::filesystem::path(std::string(str, len)); + return detail::tryConvertFromRegisteredConverter(L, index); } [[nodiscard]] static bool isInstance(lua_State* L, int index) { - return lua_type(L, index) == LUA_TSTRING; + return detail::Userdata::isInstance(L, index); } }; -#endif template [[nodiscard]] Result push(lua_State* L, const T& t) @@ -13215,6 +13438,21 @@ class Namespace : public detail::Registrar return *this; } + + template + Class& addConverter() + { + static_assert(StackConversion::enabled, + "Specialize StackConversion with enabled=true before calling addConverter()"); + + using FnType = TypeResult(*)(lua_State*, int); + static const FnType fn = &detail::convertFromStack; + + detail::getOrCreateConverterRegistry(L, lua_absindex(L, -2))->converters[detail::getClassRegistryKey()] = &fn; + detail::getOrCreateConverterRegistry(L, lua_absindex(L, -3))->converters[detail::getClassRegistryKey()] = &fn; + + return *this; + } }; class Table : public detail::Registrar diff --git a/Images/benchmarks.png b/Images/benchmarks.png index d851ad40..a56c047d 100644 Binary files a/Images/benchmarks.png and b/Images/benchmarks.png differ diff --git a/Manual.md b/Manual.md index 323ad8c5..cb665ed3 100644 --- a/Manual.md +++ b/Manual.md @@ -44,9 +44,10 @@ Contents * [2.7.2 - Index and New Index Metamethods Fallback](#272---index-and-new-index-metamethods-fallback) * [2.7.3 - Static Index and New Index Metamethods Fallback](#273---static-index-and-new-index-metamethods-fallback) * [2.8 - Lua Stack](#28---lua-stack) - * [2.8.1 - Enums](#281---enums) - * [2.8.2 - lua_State](#282---lua_state) - * [2.8.3 - Standard Library Type Conversions](#283---standard-library-type-conversions) + * [2.8.1 - Custom Type Converters](#281---custom-type-converters) + * [2.8.2 - Enums](#282---enums) + * [2.8.3 - lua_State](#283---lua_state) + * [2.8.4 - Standard Library Type Conversions](#284---standard-library-type-conversions) * [2.9 - C++20 Coroutine Integration](#29---c20-coroutine-integration) * [2.9.1 - CppCoroutine\ - Generators callable from Lua](#291---cppcoroutiner----generators-callable-from-lua) * [2.9.2 - Accepting Arguments](#292---accepting-arguments) @@ -135,6 +136,7 @@ It also offers a set of improvements compared to vanilla LuaBridge: * Transparent support of all signed and unsigned integer types up to `int64_t`. * Consistent numeric handling and conversions (signed, unsigned and floats) across all lua versions. * Simplified registration of enum types via the `luabridge::Enum` stack wrapper. +* Opt-in custom converters between registered C++ userdata types. * Opt-out handling of safe stack space checks (automatically avoids exhausting lua stack space when pushing values!). LuaBridge is distributed as a a collection of header files. You simply add one line, `#include ` where you want to pass functions, classes, and variables back and forth between C++ and Lua. There are no additional source files, no compilation settings, and no Makefiles or IDE-specific project files. LuaBridge is easy to integrate. @@ -153,7 +155,7 @@ Because LuaBridge was written with simplicity in mind there are some features th LuaBridge does not support: * Global types (types must be registered in a named scope). -* Automatic conversion between STL container types and Lua tables (but conversion can be opted in for many standard containers by including the corresponding optional header — see [2.8.3](#283---standard-library-type-conversions)) +* Automatic conversion between STL container types and Lua tables (but conversion can be opted in for many standard containers by including the corresponding optional header — see [2.8.4](#284---standard-library-type-conversions)) * Inheriting Lua classes from C++ classes. * Passing nil to a C++ function that expects a pointer or reference. @@ -1263,7 +1265,96 @@ struct Stack> } // namespace luabridge ``` -### 2.8.1 - Enums +### 2.8.1 - Custom Type Converters + +For registered class types, LuaBridge can also convert one C++ userdata type into another C++ value type while reading function arguments from Lua. This is useful when Lua should pass a lightweight or external representation to a function that expects your domain type. + +Custom converters are opt-in per target type. To enable them: + +* Specialize `luabridge::StackConversion` with `enabled = true`. +* Specialize `luabridge::StackConverter` and provide `static To convert (const From&)`. +* Register the source class with `.addConverter()`. + +```cpp +struct Vec3Source +{ + float x = 0.f; + float y = 0.f; + float z = 0.f; + + Vec3Source () = default; + Vec3Source (float x, float y, float z) : x (x), y (y), z (z) {} +}; + +struct Vec3 +{ + float x = 0.f; + float y = 0.f; + float z = 0.f; + + Vec3 () = default; + Vec3 (float x, float y, float z) : x (x), y (y), z (z) {} +}; + +float length (Vec3 v); +float lengthRef (const Vec3& v); +``` + +Define the conversion in the `luabridge` namespace: + +```cpp +namespace luabridge { + +template <> +struct StackConversion +{ + static constexpr bool enabled = true; +}; + +template <> +struct StackConverter +{ + static Vec3 convert (const Vec3Source& v) + { + return { v.x, v.y, v.z }; + } +}; + +} // namespace luabridge +``` + +Then register the source type and the converter: + +```cpp +luabridge::getGlobalNamespace (L) + .beginClass ("Vec3Source") + .addConstructor () + .addConverter () + .endClass () + .addFunction ("length", &length) + .addFunction ("lengthRef", &lengthRef); +``` + +Lua can now pass a `Vec3Source` where C++ expects `Vec3` by value or by `const Vec3&`: + +```lua +v = Vec3Source (1, 2, 3) + +length (v) +lengthRef (v) +``` + +The converter is consulted only after normal userdata extraction fails. If the Lua value already holds the requested target type, LuaBridge uses that object directly. If the value is derived from the requested target type, normal inheritance lookup is used first. The registered converter is the fallback path. + +Multiple source classes can register converters to the same target type by specializing `StackConverter` for each source type and calling `.addConverter()` on each source class. Registering the same converter more than once is harmless; the later registration replaces the earlier entry. + +Converters return an owned target value. That means converted temporaries can be passed to functions taking `To` or `const To&`. They are not a substitute for mutable references or pointers: a function that expects `To&`, `To*`, or `const To*` still requires a Lua value that actually stores the target object. + +`Stack::isInstance` remains an exact/inheritance userdata test. It does not report source userdata as instances of the target type just because a converter is registered. This matters mainly for overload selection: converters are most predictable when the overload set does not rely on `isInstance` to distinguish converted arguments. + +The target class only needs to be registered with `beginClass` if Lua must construct, store, or receive target objects directly. A converter used only to feed a C++ function argument does not require the target type to be exposed to Lua. + +### 2.8.2 - Enums In order to expose C++ enums to lua and be able to work bidirectionally with them, it's necesary to create a Stack specialization for each exposed enum. As the process might become tedious, a library wrapper class is provided to simplify the steps. @@ -1326,7 +1417,7 @@ luabridge::getGlobalNamespace (L) .endNamespace(); ``` -### 2.8.2 - lua_State +### 2.8.3 - lua_State Sometimes it is convenient from within a bound function or member function to gain access to the `lua_State*` normally available to a lua_CFunction. With LuaBridge, all you need to do is add a `lua_State*` as the last parameter of your bound function: @@ -1348,7 +1439,7 @@ When the script calls `useStateAndArgs`, it passes only the integer and string p The same is applicable for properties. -### 2.8.3 - Standard Library Type Conversions +### 2.8.4 - Standard Library Type Conversions LuaBridge does not enable STL container-to-Lua-table conversions by default. Each supported container type has its own optional header that must be included explicitly. All conversions map the container to a Lua table and back. @@ -1379,6 +1470,7 @@ The table below lists every optional header and its requirements: **Example — using `std::deque` and `std::multimap`:** +{% raw %} ```cpp #include #include @@ -1393,6 +1485,7 @@ luabridge::push(L, dq); std::multimap mm = {{"a", 1}, {"a", 2}, {"b", 3}}; luabridge::push(L, mm); ``` +{% endraw %} **Example — push-only `std::any`:** @@ -2661,6 +2754,15 @@ template Class addStaticNewIndexMetaMethod (Function function); ``` +### Converter Registration + +```cpp +/// Registers a converter from this class type T to target type To. +/// Requires StackConversion::enabled and StackConverter. +template +Class addConverter (); +``` + Lua Variable Reference - LuaRef ------------------------------- @@ -2850,6 +2952,20 @@ Stack Traits - Stack ----------------------- ```cpp +/// Opt-in trait for enabling registered converter lookup for target type T. +template +struct StackConversion +{ + static constexpr bool enabled = false; +}; + +/// User-specialized conversion hook from source type From to target type To. +template +struct StackConverter +{ + static To convert (const From& from); +}; + /// Converts the C++ value into the Lua value at the top of the Lua stack. Returns true if the push could be performed. /// When false is returned, `ec` contains the error code corresponding to the failure. Result push (lua_State* L, const T& value); diff --git a/README.md b/README.md index aee11ca0..796cec51 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,10 @@ LuaBridge3 is usable from a compliant C++17 compiler and offers the following fe LuaBridge3 has been heavily optimized and now competes directly with [sol2](https://github.com/ThePhD/sol2) — one of the fastest C++/Lua binding libraries — across most workloads. +Benchmarks measure the overhead each library adds on top of the plain Lua C API: the cost of abstracting and wrapping it for C++ use. All libraries are compiled together with the benchmark executable (no separate Lua DLL) so that inlining and link-time optimizations reflect real-world usage scenarios. Every library runs with its maximum safety settings enabled — the numbers represent what you actually ship, not a stripped-down, crash-prone configuration. + +The benchmark suite covers common operations: global and table access, free and member function calls, userdata property access across class hierarchies, shared-pointer ownership, multi-return functions, lambda captures, custom type converters. Each case is run in isolation so the numbers are directly comparable between libraries. + ![Benchmarks](./Images/benchmarks.png) ## Improvements Over Vanilla LuaBridge diff --git a/Source/LuaBridge/detail/ClassInfo.h b/Source/LuaBridge/detail/ClassInfo.h index ad90f244..82e48999 100644 --- a/Source/LuaBridge/detail/ClassInfo.h +++ b/Source/LuaBridge/detail/ClassInfo.h @@ -186,6 +186,15 @@ template ().find_first_of('.')> return reinterpret_cast(0x8108); } +//================================================================================================= +/** + * @brief The key of a ConverterRegistry userdata in a class metatable. + */ +[[nodiscard]] inline const void* getConvertersKey() noexcept +{ + return reinterpret_cast(0xc0de); +} + //================================================================================================= /** * The key of the static index fall back in another metatable. diff --git a/Source/LuaBridge/detail/Converter.h b/Source/LuaBridge/detail/Converter.h new file mode 100644 index 00000000..f2bafcf4 --- /dev/null +++ b/Source/LuaBridge/detail/Converter.h @@ -0,0 +1,183 @@ +// https://github.com/kunitoki/LuaBridge3 +// Copyright 2026, kunitoki +// SPDX-License-Identifier: MIT + +#pragma once + +#include "ClassInfo.h" +#include "Errors.h" +#include "LuaHelpers.h" +#include "Result.h" + +#include +#include +#include +#include +#include + +namespace luabridge { + +// Forward declaration. +template +struct Stack; + +//================================================================================================= +/** + * @brief Opt-in trait for enabling custom type converters for type T. + * + * Specialize with enabled = true so that Stack::get consults the metatable + * converter registry as a Phase 3 fallback after Phase 1 (exact match) and + * Phase 2 (inheritance) both fail. + * + * Example: + * template <> struct luabridge::StackConversion { static constexpr bool enabled = true; }; + */ +template +struct StackConversion +{ + static constexpr bool enabled = false; +}; + +//================================================================================================= +/** + * @brief User-defined conversion hook from source type From to target type To. + * + * Specialize this template for each (To, From) pair and provide: + * static To convert(const From& from); + * + * Example: + * template <> + * struct luabridge::StackConverter { + * static Vec3 convert(const glm::vec3& v) { return {v.x, v.y, v.z}; } + * }; + */ +template +struct StackConverter; + +//================================================================================================= + +namespace detail { + +struct ConverterRegistry +{ + std::unordered_map converters; +}; + +inline ConverterRegistry* getOrCreateConverterRegistry(lua_State* L, int metatableIdx) +{ + const int absIdx = lua_absindex(L, metatableIdx); + + lua_rawgetp_x(L, absIdx, getConvertersKey()); + if (lua_isuserdata(L, -1) && !lua_islightuserdata(L, -1)) + { + auto* reg = align(lua_touserdata(L, -1)); + lua_pop(L, 1); + return reg; + } + lua_pop(L, 1); // pop nil or unexpected value + + // Create ConverterRegistry as an aligned Lua full userdata with automatic __gc + lua_newuserdata_aligned(L); + auto* reg = align(lua_touserdata(L, -1)); + + // Store the userdata in the class metatable + lua_pushvalue(L, -1); // dup userdata + lua_rawsetp_x(L, absIdx, getConvertersKey()); // store, pops dup + lua_pop(L, 1); // pop the original userdata + + return reg; +} + +template +class ConverterConstRef +{ +public: + explicit ConverterConstRef(const T& value) noexcept + : m_ref(std::addressof(value)) + { + } + + explicit ConverterConstRef(T&& value) noexcept(std::is_nothrow_move_constructible_v) + : m_value(std::move(value)) + , m_ref(std::addressof(*m_value)) + { + } + + ConverterConstRef(ConverterConstRef&& other) noexcept(std::is_nothrow_move_constructible_v) + : m_value(std::move(other.m_value)) + , m_ref(m_value.has_value() ? std::addressof(*m_value) : other.m_ref) + { + } + + ConverterConstRef(const ConverterConstRef& other) + : m_value(other.m_value) + , m_ref(m_value.has_value() ? std::addressof(*m_value) : other.m_ref) + { + } + + ConverterConstRef& operator=(ConverterConstRef&& other) noexcept(std::is_nothrow_move_assignable_v) + { + m_value = std::move(other.m_value); + m_ref = m_value.has_value() ? std::addressof(*m_value) : other.m_ref; + return *this; + } + + ConverterConstRef& operator=(const ConverterConstRef& other) + { + m_value = other.m_value; + m_ref = m_value.has_value() ? std::addressof(*m_value) : other.m_ref; + return *this; + } + + operator const T&() const noexcept + { + return *m_ref; + } + +private: + std::optional m_value; + const T* m_ref = nullptr; +}; + +template +TypeResult tryConvertFromRegisteredConverter(lua_State* L, int index) +{ + using FnType = TypeResult(*)(lua_State*, int); + + if (! lua_getmetatable(L, index)) + return makeErrorCode(ErrorCode::InvalidTypeCast); + + lua_rawgetp_x(L, -1, detail::getConvertersKey()); + if (lua_isuserdata(L, -1) && !lua_islightuserdata(L, -1)) + { + auto* reg = align(lua_touserdata(L, -1)); + lua_pop(L, 2); // registry userdata + metatable + + auto it = reg->converters.find(detail::getClassRegistryKey()); + if (it != reg->converters.end() && it->second) + { + const auto* fn = static_cast(it->second); + return (*fn)(L, index); + } + } + else + { + lua_pop(L, 2); // nil/other + metatable + } + + return makeErrorCode(ErrorCode::InvalidTypeCast); +} + +template +TypeResult convertFromStack(lua_State* L, int index) +{ + auto result = detail::Userdata::get(L, index, true); + + if (!result || !*result) + return makeErrorCode(ErrorCode::InvalidTypeCast); + + return StackConverter::convert(**result); +} + +} // namespace detail +} // namespace luabridge diff --git a/Source/LuaBridge/detail/Namespace.h b/Source/LuaBridge/detail/Namespace.h index f76774cb..3bcdc9d4 100644 --- a/Source/LuaBridge/detail/Namespace.h +++ b/Source/LuaBridge/detail/Namespace.h @@ -1559,6 +1559,37 @@ class Namespace : public detail::Registrar return *this; } + + //========================================================================================= + /** + * @brief Register a custom type converter from this class (T) to a target type (To). + * + * Stores a function pointer in the FROM class's (T's) Lua metatable under + * getConvertersKey() → getClassRegistryKey(), for both the class table and + * the const table. Phase 3 in Stack::get reads it back during extraction. + * + * Requirements: + * - StackConversion::enabled must be true (user specializes to opt in). + * - StackConverter must provide: static To convert(const T&). + * + * @tparam To Target type. Stack and Stack gain Phase 3 fallback. + */ + template + Class& addConverter() + { + static_assert(StackConversion::enabled, + "Specialize StackConversion with enabled=true before calling addConverter()"); + + using FnType = TypeResult(*)(lua_State*, int); + static const FnType fn = &detail::convertFromStack; + + // Stack during Class methods: ns, co, cl, st + // Store into both cl (-2) and co (-3) so both mutable and const userdatas work. + detail::getOrCreateConverterRegistry(L, lua_absindex(L, -2))->converters[detail::getClassRegistryKey()] = &fn; // cl + detail::getOrCreateConverterRegistry(L, lua_absindex(L, -3))->converters[detail::getClassRegistryKey()] = &fn; // co + + return *this; + } }; class Table : public detail::Registrar diff --git a/Source/LuaBridge/detail/Stack.h b/Source/LuaBridge/detail/Stack.h index b1c24641..b29c9be9 100644 --- a/Source/LuaBridge/detail/Stack.h +++ b/Source/LuaBridge/detail/Stack.h @@ -12,6 +12,7 @@ #include "Expected.h" #include "Result.h" #include "Userdata.h" +#include "Converter.h" #include #include @@ -1464,16 +1465,56 @@ struct Stack } }; +#if LUABRIDGE_HAS_CXX17_FILESYSTEM + +//================================================================================================= +/** + * @brief Stack specialization for `std::filesystem::path`. + */ +template <> +struct Stack +{ + [[nodiscard]] static Result push(lua_State* L, const std::filesystem::path& path) + { +#if LUABRIDGE_SAFE_STACK_CHECKS + if (! lua_checkstack(L, 1)) + return makeErrorCode(ErrorCode::LuaStackOverflow); +#endif + + lua_pushlstring(L, path.string().c_str(), path.string().size()); + return {}; + } + + [[nodiscard]] static TypeResult get(lua_State* L, int index) + { + if (lua_type(L, index) != LUA_TSTRING) + return makeErrorCode(ErrorCode::InvalidTypeCast); + + std::size_t len = 0; + const char* str = lua_tolstring(L, index, &len); + return std::filesystem::path(std::string(str, len)); + } + + [[nodiscard]] static bool isInstance(lua_State* L, int index) + { + return lua_type(L, index) == LUA_TSTRING; + } +}; +#endif // LUABRIDGE_HAS_CXX17_FILESYSTEM + //================================================================================================= namespace detail { template -struct StackOpSelector +struct StackOpSelector { using ReturnType = TypeResult; - static Result push(lua_State* L, T& value) { return Stack::push(L, value); } + static Result push(lua_State* L, T* value) + { + return value ? Stack::push(L, *value) : Stack::push(L, nullptr); + } static ReturnType get(lua_State* L, int index) { return Stack::get(L, index); } @@ -1481,11 +1522,14 @@ struct StackOpSelector }; template -struct StackOpSelector +struct StackOpSelector { using ReturnType = TypeResult; - static Result push(lua_State* L, const T& value) { return Stack::push(L, value); } + static Result push(lua_State* L, const T* value) + { + return value ? Stack::push(L, *value) : Stack::push(L, nullptr); + } static ReturnType get(lua_State* L, int index) { return Stack::get(L, index); } @@ -1493,14 +1537,11 @@ struct StackOpSelector }; template -struct StackOpSelector +struct StackOpSelector { using ReturnType = TypeResult; - static Result push(lua_State* L, T* value) - { - return value ? Stack::push(L, *value) : Stack::push(L, nullptr); - } + static Result push(lua_State* L, T& value) { return Stack::push(L, value); } static ReturnType get(lua_State* L, int index) { return Stack::get(L, index); } @@ -1508,14 +1549,11 @@ struct StackOpSelector }; template -struct StackOpSelector +struct StackOpSelector { using ReturnType = TypeResult; - static Result push(lua_State* L, const T* value) - { - return value ? Stack::push(L, *value) : Stack::push(L, nullptr); - } + static Result push(lua_State* L, const T& value) { return Stack::push(L, value); } static ReturnType get(lua_State* L, int index) { return Stack::get(L, index); } @@ -1525,25 +1563,25 @@ struct StackOpSelector } // namespace detail template -struct Stack>> +struct Stack { - using Helper = detail::StackOpSelector::value>; + using Helper = detail::StackOpSelector::value>; using ReturnType = typename Helper::ReturnType; - [[nodiscard]] static Result push(lua_State* L, T& value) { return Helper::push(L, value); } + [[nodiscard]] static Result push(lua_State* L, T* value) { return Helper::push(L, value); } [[nodiscard]] static ReturnType get(lua_State* L, int index) { return Helper::get(L, index); } [[nodiscard]] static bool isInstance(lua_State* L, int index) { return Helper::isInstance(L, index); } }; -template -struct Stack>> +template +struct Stack { - using Helper = detail::StackOpSelector::value>; + using Helper = detail::StackOpSelector::value>; using ReturnType = typename Helper::ReturnType; - [[nodiscard]] static Result push(lua_State* L, const T& value) { return Helper::push(L, value); } + [[nodiscard]] static Result push(lua_State* L, const T* value) { return Helper::push(L, value); } [[nodiscard]] static ReturnType get(lua_State* L, int index) { return Helper::get(L, index); } @@ -1551,67 +1589,105 @@ struct Stack>> }; template -struct Stack +struct Stack && !std::is_const_v>> { - using Helper = detail::StackOpSelector::value>; + using Helper = detail::StackOpSelector::value>; using ReturnType = typename Helper::ReturnType; - [[nodiscard]] static Result push(lua_State* L, T* value) { return Helper::push(L, value); } + [[nodiscard]] static Result push(lua_State* L, T& value) { return Helper::push(L, value); } [[nodiscard]] static ReturnType get(lua_State* L, int index) { return Helper::get(L, index); } [[nodiscard]] static bool isInstance(lua_State* L, int index) { return Helper::isInstance(L, index); } }; -template -struct Stack +template +struct Stack && !StackConversion::enabled>> { - using Helper = detail::StackOpSelector::value>; + using Helper = detail::StackOpSelector::value>; using ReturnType = typename Helper::ReturnType; - [[nodiscard]] static Result push(lua_State* L, const T* value) { return Helper::push(L, value); } + [[nodiscard]] static Result push(lua_State* L, const T& value) { return Helper::push(L, value); } [[nodiscard]] static ReturnType get(lua_State* L, int index) { return Helper::get(L, index); } [[nodiscard]] static bool isInstance(lua_State* L, int index) { return Helper::isInstance(L, index); } }; -#if LUABRIDGE_HAS_CXX17_FILESYSTEM +template +struct Stack::enabled && !std::is_array_v>> +{ + using ReturnType = TypeResult>; -//================================================================================================= -/** - * @brief Stack specialization for `std::filesystem::path`. - */ -template <> -struct Stack + [[nodiscard]] static Result push(lua_State* L, const T& value) + { + return Stack::push(L, value); + } + + [[nodiscard]] static ReturnType get(lua_State* L, int index) + { + if (detail::Userdata::isInstance(L, index)) + { + auto result = detail::Userdata::get(L, index, true); + if (!result) + return result.error(); + + if (T* ptr = *result) + return detail::ConverterConstRef(*ptr); + + return detail::getNilBadArgError(L, index); + } + + auto converted = detail::tryConvertFromRegisteredConverter(L, index); + if (!converted) + return converted.error(); + + return detail::ConverterConstRef(std::move(*converted)); + } + + [[nodiscard]] static bool isInstance(lua_State* L, int index) + { + return Stack::isInstance(L, index); + } +}; + +template +struct Stack::enabled>> { - [[nodiscard]] static Result push(lua_State* L, const std::filesystem::path& path) + using IsUserdata = void; + using ReturnType = TypeResult; + + [[nodiscard]] static Result push(lua_State* L, const T& value) { -#if LUABRIDGE_SAFE_STACK_CHECKS - if (! lua_checkstack(L, 1)) - return makeErrorCode(ErrorCode::LuaStackOverflow); -#endif + return detail::StackHelper::value>::push(L, value); + } - lua_pushlstring(L, path.string().c_str(), path.string().size()); - return {}; + [[nodiscard]] static Result push(lua_State* L, T&& value) + { + return detail::StackHelper::value>::push(L, std::move(value)); } - [[nodiscard]] static TypeResult get(lua_State* L, int index) + [[nodiscard]] static ReturnType get(lua_State* L, int index) { - if (lua_type(L, index) != LUA_TSTRING) - return makeErrorCode(ErrorCode::InvalidTypeCast); + if (detail::Userdata::isInstance(L, index)) + { + auto result = detail::Userdata::get(L, index, true); + if (result) + { + if (T* ptr = *result) + return *ptr; + } + return result.error(); + } - std::size_t len = 0; - const char* str = lua_tolstring(L, index, &len); - return std::filesystem::path(std::string(str, len)); + return detail::tryConvertFromRegisteredConverter(L, index); } [[nodiscard]] static bool isInstance(lua_State* L, int index) { - return lua_type(L, index) == LUA_TSTRING; + return detail::Userdata::isInstance(L, index); } }; -#endif // LUABRIDGE_HAS_CXX17_FILESYSTEM //================================================================================================= /** diff --git a/Source/LuaBridge/detail/Userdata.h b/Source/LuaBridge/detail/Userdata.h index e02b49ec..ace0d24a 100644 --- a/Source/LuaBridge/detail/Userdata.h +++ b/Source/LuaBridge/detail/Userdata.h @@ -297,11 +297,7 @@ class Userdata const auto classId = detail::getClassRegistryKey(); const auto constId = detail::getConstRegistryKey(); - // Common-case fast path: compare the object's metatable directly against - // the registry class/const tables. This avoids an extra interior table - // lookup (getTypeIdentityKey) and the lua_istable guard (lua_getmetatable - // always pushes a table when it returns non-zero). Safe because scripts - // cannot set metatables on userdata (Lua security model, points 1-3). + // Common-case fast path: compare the object's metatable directly against the registry class/const tables. if (lua_getmetatable(L, absIndex)) // Stack: ..., mt { lua_rawgetp_x(L, LUA_REGISTRYINDEX, classId); // Stack: ..., mt, class_mt diff --git a/Tests/CMakeLists.txt b/Tests/CMakeLists.txt index 8bf0c895..9dda1be3 100644 --- a/Tests/CMakeLists.txt +++ b/Tests/CMakeLists.txt @@ -32,6 +32,7 @@ set (LUABRIDGE_TEST_SOURCE_FILES Source/ArrayTests.cpp Source/ClassExtensibleTests.cpp Source/ClassTests.cpp + Source/ConverterTests.cpp Source/CoroutineTests.cpp Source/DequeTests.cpp Source/DumpTests.cpp diff --git a/Tests/Source/ConverterTests.cpp b/Tests/Source/ConverterTests.cpp new file mode 100644 index 00000000..95361e87 --- /dev/null +++ b/Tests/Source/ConverterTests.cpp @@ -0,0 +1,383 @@ +// https://github.com/kunitoki/LuaBridge3 +// Copyright 2024, kunitoki +// SPDX-License-Identifier: MIT + +#include "TestBase.h" + +#include + +struct ConverterTests : TestBase +{ +}; + +namespace { + +//================================================================================================= +// Source types — registered with LuaBridge; addConverter points FROM these +struct Vec3Source +{ + float x = 0.f, y = 0.f, z = 0.f; + Vec3Source() = default; + Vec3Source(float x, float y, float z) : x(x), y(y), z(z) {} +}; + +struct ColorSource +{ + float r = 0.f, g = 0.f, b = 0.f; + ColorSource() = default; + ColorSource(float r, float g, float b) : r(r), g(g), b(b) {} +}; + +// Source with inheritance — used to test that addConverter on base propagates correctly +struct BaseSource +{ + int base_val = 0; + BaseSource() = default; + explicit BaseSource(int v) : base_val(v) {} +}; + +struct DerivedSource : BaseSource +{ + int derived_val = 0; + DerivedSource() = default; + DerivedSource(int b, int d) : BaseSource(b), derived_val(d) {} +}; + +// Multiple-inheritance source — tests pointer adjustment in converter +struct OffsetBase +{ + int padding = 999; // ensures DerivedMulti has non-zero offset to BaseSource +}; + +struct DerivedMulti : OffsetBase, BaseSource +{ + DerivedMulti() = default; + DerivedMulti(int b) : BaseSource(b) {} +}; + +//================================================================================================= +// Target types — StackConversion::enabled = true; converters produce these +struct Vec3Target +{ + float x = 0.f, y = 0.f, z = 0.f; + Vec3Target() = default; + Vec3Target(float x, float y, float z) : x(x), y(y), z(z) {} + bool operator==(const Vec3Target& o) const { return x == o.x && y == o.y && z == o.z; } +}; + +struct MultiTarget +{ + int value = 0; + MultiTarget() = default; + explicit MultiTarget(int v) : value(v) {} + bool operator==(const MultiTarget& o) const { return value == o.value; } +}; + +} // namespace + +// Opt-in traits +namespace luabridge { +template <> struct StackConversion { static constexpr bool enabled = true; }; +template <> struct StackConversion { static constexpr bool enabled = true; }; +} // namespace luabridge + +// Converters +namespace luabridge { +template <> +struct StackConverter +{ + static Vec3Target convert(const Vec3Source& s) { return {s.x, s.y, s.z}; } +}; + +template <> +struct StackConverter +{ + static MultiTarget convert(const Vec3Source& s) { return MultiTarget{static_cast(s.x)}; } +}; + +template <> +struct StackConverter +{ + static MultiTarget convert(const ColorSource& s) { return MultiTarget{static_cast(s.r * 255)}; } +}; + +template <> +struct StackConverter +{ + static MultiTarget convert(const BaseSource& s) { return MultiTarget{s.base_val}; } +}; + +template <> +struct StackConverter +{ + static MultiTarget convert(const DerivedSource& s) { return MultiTarget{s.base_val}; } +}; + +template <> +struct StackConverter +{ + // DerivedMulti : OffsetBase, BaseSource — base_val is at non-zero offset + static MultiTarget convert(const DerivedMulti& s) { return MultiTarget{s.base_val}; } +}; +} // namespace luabridge + +namespace { + +//================================================================================================= +// Free functions that accept the target types +static float sumVec3(Vec3Target v) { return v.x + v.y + v.z; } +static float sumVec3Ref(const Vec3Target& v) { return v.x + v.y + v.z; } +static const Vec3Target* capturedVec3Ref = nullptr; +static float captureVec3Ref(const Vec3Target& v) +{ + capturedVec3Ref = &v; + return sumVec3Ref(v); +} +static int getMultiValue(MultiTarget t) { return t.value; } + +void registerAll(lua_State* L) +{ + luabridge::getGlobalNamespace(L) + .beginClass("Vec3Source") + .addConstructor() + .addConverter() + .addConverter() + .endClass() + .beginClass("ColorSource") + .addConstructor() + .addConverter() + .endClass() + .beginClass("BaseSource") + .addConstructor() + .addConverter() + .endClass() + .deriveClass("DerivedSource") + .addConstructor() + .addConverter() + .endClass() + .deriveClass("DerivedMulti") + .addConstructor() + .addConverter() + .endClass() + .beginClass("Vec3Target") + .addConstructor() + .endClass() + .beginClass("MultiTarget") + .endClass() + .addFunction("sumVec3", sumVec3) + .addFunction("sumVec3Ref", sumVec3Ref) + .addFunction("captureVec3Ref", captureVec3Ref) + .addFunction("getMultiValue", getMultiValue); +} + +} // namespace + +//================================================================================================= +// Diagnostic: verify the converters sub-table is actually stored in cl + +TEST_F(ConverterTests, Diagnostic_MetatableHasConverters) +{ + registerAll(L); + + // Check that Vec3Source's class table has a ConverterRegistry userdata + luabridge::lua_rawgetp_x(L, LUA_REGISTRYINDEX, luabridge::detail::getClassRegistryKey()); + ASSERT_TRUE(lua_istable(L, -1)) << "Vec3Source cl not found in registry"; + + luabridge::lua_rawgetp_x(L, -1, luabridge::detail::getConvertersKey()); + EXPECT_TRUE(lua_isuserdata(L, -1) && !lua_islightuserdata(L, -1)) + << "Expected ConverterRegistry full userdata in Vec3Source cl (type=" << lua_type(L, -1) << ")"; + + if (lua_isuserdata(L, -1) && !lua_islightuserdata(L, -1)) { + auto* reg = luabridge::align(lua_touserdata(L, -1)); + EXPECT_NE(reg->converters.count(luabridge::detail::getClassRegistryKey()), 0u) + << "No Vec3Target converter in ConverterRegistry"; + } + lua_pop(L, 2); // registry userdata + cl +} + +TEST_F(ConverterTests, Diagnostic_DirectStackGet) +{ + registerAll(L); + + // Push a Vec3Source into Lua, then get it as a Vec3Target via Phase 3 + runLua("_src = Vec3Source(1, 2, 3)"); + lua_getglobal(L, "_src"); + ASSERT_TRUE(lua_isuserdata(L, -1)) << "Expected Vec3Source userdata, got type=" << lua_type(L, -1); + + // Verify its metatable has the ConverterRegistry userdata + lua_getmetatable(L, -1); + ASSERT_TRUE(lua_istable(L, -1)) << "No metatable on Vec3Source"; + luabridge::lua_rawgetp_x(L, -1, luabridge::detail::getConvertersKey()); + EXPECT_TRUE(lua_isuserdata(L, -1) && !lua_islightuserdata(L, -1)) + << "Userdata metatable has no ConverterRegistry (type=" << lua_type(L, -1) << ")"; + lua_pop(L, 2); // registry userdata + metatable + + // Try Stack::get directly on the userdata + auto res = luabridge::Stack::get(L, -1); + EXPECT_TRUE(static_cast(res)) << "Stack::get failed"; + if (res) { + EXPECT_FLOAT_EQ((*res).x + (*res).y + (*res).z, 6.0f); + } + + lua_pop(L, 1); // pop _src +} + +//================================================================================================= +// Phase 1+2 still work (exact match is unaffected by converter registration) + +TEST_F(ConverterTests, ExactTypePassThrough) +{ + registerAll(L); + runLua("result = sumVec3(Vec3Target(1, 2, 3))"); + EXPECT_FLOAT_EQ(result(), 6.f); +} + +TEST_F(ConverterTests, ExactTypeConstRefUsesUserdataReference) +{ + registerAll(L); + capturedVec3Ref = nullptr; + + runLua("_target = Vec3Target(1, 2, 3)"); + lua_getglobal(L, "_target"); + auto target = luabridge::detail::Userdata::get(L, -1, true); + ASSERT_TRUE(target); + ASSERT_NE(*target, nullptr); + Vec3Target* targetPtr = *target; + lua_pop(L, 1); + + runLua("result = captureVec3Ref(_target)"); + EXPECT_FLOAT_EQ(result(), 6.f); + EXPECT_EQ(capturedVec3Ref, targetPtr); +} + +//================================================================================================= +// Phase 3: value conversion (function taking T by value) + +TEST_F(ConverterTests, ValueConversionBasic) +{ + registerAll(L); + runLua("result = sumVec3(Vec3Source(1, 2, 3))"); + EXPECT_FLOAT_EQ(result(), 6.f); +} + +TEST_F(ConverterTests, ValueConversionCorrectFields) +{ + registerAll(L); + runLua("result = sumVec3(Vec3Source(10, 20, 30))"); + EXPECT_FLOAT_EQ(result(), 60.f); +} + +//================================================================================================= +// Phase 3: const-ref conversion (function taking const T&) + +TEST_F(ConverterTests, ConstRefConversion) +{ + registerAll(L); + runLua("result = sumVec3Ref(Vec3Source(4, 5, 6))"); + EXPECT_FLOAT_EQ(result(), 15.f); +} + +//================================================================================================= +// Multiple converters to the same target type (from different sources) + +TEST_F(ConverterTests, MultipleSourcesToSameTarget_Vec3Source) +{ + registerAll(L); + runLua("result = getMultiValue(Vec3Source(7, 0, 0))"); + EXPECT_EQ(result(), 7); +} + +TEST_F(ConverterTests, MultipleSourcesToSameTarget_ColorSource) +{ + registerAll(L); + // r=1.0 → int(1.0 * 255) = 255 + runLua("result = getMultiValue(ColorSource(1, 0, 0))"); + EXPECT_EQ(result(), 255); +} + +//================================================================================================= +// Derived class with its own converter; the converter internally uses Userdata::get +// which triggers Phase 2 (inheritance) for pointer adjustment. + +TEST_F(ConverterTests, DerivedSourceWithRegisteredConverter) +{ + registerAll(L); + // DerivedSource(base=42, derived=99) — converter reads BaseSource::base_val = 42 + runLua("result = getMultiValue(DerivedSource(42, 99))"); + EXPECT_EQ(result(), 42); +} + +//================================================================================================= +// Multiple inheritance: DerivedMulti inherits from both OffsetBase and BaseSource. +// The BaseSource sub-object is at a non-zero offset; the cast table must adjust the pointer. + +TEST_F(ConverterTests, MultipleInheritancePointerAdjustment) +{ + registerAll(L); + // DerivedMulti(base=77) — converter reads BaseSource::base_val = 77 after offset adjustment + runLua("result = getMultiValue(DerivedMulti(77))"); + EXPECT_EQ(result(), 77); +} + +//================================================================================================= +// Wrong type should fail (not a userdata that has any registered converter to Vec3Target) + +TEST_F(ConverterTests, WrongTypeFails) +{ + registerAll(L); + // ColorSource has no converter to Vec3Target — should produce an error + auto [ok, _] = runLuaCaptureError("result = sumVec3(ColorSource(1, 2, 3))"); + EXPECT_FALSE(ok); +} + +//================================================================================================= +// Nil should fail for value target + +TEST_F(ConverterTests, NilFails) +{ + registerAll(L); + auto [ok, _] = runLuaCaptureError("result = sumVec3(nil)"); + EXPECT_FALSE(ok); +} + +//================================================================================================= +// Registering same converter twice is idempotent (second registration overwrites first) + +TEST_F(ConverterTests, DoubleRegistration) +{ + luabridge::getGlobalNamespace(L) + .beginClass("Vec3Source") + .addConstructor() + .addConverter() + .addConverter() // second call — should overwrite, not duplicate + .endClass() + .beginClass("Vec3Target") + .addConstructor() + .endClass() + .addFunction("sumVec3", sumVec3); + + runLua("result = sumVec3(Vec3Source(1, 2, 3))"); + EXPECT_FLOAT_EQ(result(), 6.f); +} + +//================================================================================================= +// Const userdata object should also work (converter stored in const table) + +TEST_F(ConverterTests, ConstUserdataConverter) +{ + // Expose a function that returns a const reference to a Vec3Source stored in C++ + static Vec3Source constSrc{3.f, 4.f, 5.f}; + + luabridge::getGlobalNamespace(L) + .beginClass("Vec3Source") + .addConverter() + .endClass() + .beginClass("Vec3Target") + .addConstructor() + .endClass() + .addFunction("getConstSrc", []() -> const Vec3Source& { return constSrc; }) + .addFunction("sumVec3", sumVec3); + + runLua("result = sumVec3(getConstSrc())"); + EXPECT_FLOAT_EQ(result(), 12.f); +} diff --git a/amalgamate.py b/amalgamate.py index 70181b54..a96b78cf 100644 --- a/amalgamate.py +++ b/amalgamate.py @@ -218,7 +218,7 @@ def WriteAlgamationFiles(self): headerAmalgamation.write(f"#include <{header}>\n") headerAmalgamation.write("\n") - for header in systemHeaders: + for header in reversed(sorted(systemHeaders)): guard = GetGuardedInclude(header) if guard is not None: headerAmalgamation.write(f"#if defined(__has_include) && __has_include(<{header}>)") diff --git a/justfile b/justfile index d7f209a5..996c1ae9 100644 --- a/justfile +++ b/justfile @@ -22,6 +22,14 @@ test-all: @just test 20 @just test 23 +build1 CXX="17": + @just generate {{CXX}} + cmake --build Build{{CXX}} --config Debug --target LuaBridgeTests54 -j8 + +test1 CXX="17": + @just build1 {{CXX}} + ./Build{{CXX}}/Tests/Debug/LuaBridgeTests54 + sanitize TYPE="address" CXX="17": cmake -G Xcode -B Build{{CXX}} -DLUABRIDGE_SANITIZE={{TYPE}} .