aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBenoit Germain <bnt.germain@gmail.com>2025-09-20 14:30:57 +0200
committerBenoit Germain <bnt.germain@gmail.com>2025-09-20 14:30:57 +0200
commitc52571736d852d2636bd285d19c613be5c706cff (patch)
tree50a1fc9b867197faa695c728008c8d0c7498b756
parentdc3de6ef8d4bb1a8ce7b2932515a0f6287ab1e76 (diff)
downloadlanes-c52571736d852d2636bd285d19c613be5c706cff.tar.gz
lanes-c52571736d852d2636bd285d19c613be5c706cff.tar.bz2
lanes-c52571736d852d2636bd285d19c613be5c706cff.zip
Improve table and userdata conversions
* add convert_fallback and convert_max_attempts to global settings * if no __lanesconvert is available, use convert_fallback (can be useful for externally provided full userdata with fixed metatables) * only try conversion on non-deep and non-clonable userdata * conversion can be applied recursively, up to convert_max_attempts times * plus all the relevant unit tests of course
-rw-r--r--docs/index.html33
-rw-r--r--src/intercopycontext.cpp298
-rw-r--r--src/intercopycontext.hpp32
-rw-r--r--src/lanes.cpp3
-rw-r--r--src/lanes.lua28
-rw-r--r--src/universe.cpp16
-rw-r--r--src/universe.hpp14
-rw-r--r--unit_tests/UnitTests.vcxproj1
-rw-r--r--unit_tests/UnitTests.vcxproj.filters1
-rw-r--r--unit_tests/init_and_shutdown.cpp161
-rw-r--r--unit_tests/misc_tests.cpp193
-rw-r--r--unit_tests/shared.cpp8
12 files changed, 656 insertions, 132 deletions
diff --git a/docs/index.html b/docs/index.html
index 2e74495..0d24d55 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -71,7 +71,7 @@
71 </p> 71 </p>
72 72
73 <p> 73 <p>
74 This document was revised on 03-Jul-25, and applies to version <tt>4.0.0</tt>. 74 This document was revised on 19-Sep-25, and applies to version <tt>4.0.0</tt>.
75 </p> 75 </p>
76 </font> 76 </font>
77 </center> 77 </center>
@@ -388,6 +388,30 @@
388 </tr> 388 </tr>
389 389
390 <tr valign=top> 390 <tr valign=top>
391 <td id="convert_fallback">
392 <code>.convert_fallback</code>
393 </td>
394 <td>
395 <tt>nil</tt>, <tt>lanes.null</tt> or <tt>'decay'</tt>
396 </td>
397 <td>
398 Same as <tt>__lanesconvert</tt> (see <a href="#other">Limitations on data passing</a>), but cannot be a function, because this would have to be transferred to all newly created lanes.
399 </td>
400 </tr>
401
402 <tr valign=top>
403 <td id="convert_max_attempts">
404 <code>.convert_max_attempts</code>
405 </td>
406 <td>
407 1 &ge; number &ge; 9
408 </td>
409 <td>
410 When a table or userdata conversion is performed with a function, it can happen recursively up to this number of attempts before triggering an error. Default is 1.
411 </td>
412 </tr>
413
414 <tr valign=top>
391 <td id="internal_allocator"> 415 <td id="internal_allocator">
392 <code>.internal_allocator</code> 416 <code>.internal_allocator</code>
393 </td> 417 </td>
@@ -1801,11 +1825,14 @@
1801 Using the same source table in multiple <a href="#lindas">linda</a> messages keeps no ties between the tables (this is the same reason why tables can't be used as slots). 1825 Using the same source table in multiple <a href="#lindas">linda</a> messages keeps no ties between the tables (this is the same reason why tables can't be used as slots).
1802 </li> 1826 </li>
1803 <li> 1827 <li>
1804 For tables and full userdata: before anything else, the metatable is searched for a <tt>__lanesconvert</tt> field. If found, the source object is converted as follows depending on <tt>__lanesconvert</tt>'s value: 1828 For tables and full userdata that are neither deep nor clonable: before anything else, a converter is searched for in the <tt>__lanesconvert</tt> field of its metatable.
1829 If there is no metatable, or no <tt>__lanesconvert</tt>, <a href="#convert_fallback"><tt>convert_fallback</tt></a> is used instead.
1830 The source object is then converted depending on the converter value:
1805 <ul> 1831 <ul>
1832 <li><tt>nil</tt>: The value is not converted. If it is not a clonable or deep userdata, the transfer will eventually fail with an error.</li>
1806 <li><tt>lanes.null</tt>: The value is converted to <tt>nil</tt>.</li> 1833 <li><tt>lanes.null</tt>: The value is converted to <tt>nil</tt>.</li>
1807 <li><tt>"decay"</tt>: The value is converted to a light userdata obtained from <tt>lua_topointer()</tt>.</li> 1834 <li><tt>"decay"</tt>: The value is converted to a light userdata obtained from <tt>lua_topointer()</tt>.</li>
1808 <li>A function: The function is called as <tt>o:__lanesconvert(string)</tt>, where the argument is either <tt>"keeper"</tt> or <tt>"regular"</tt>, depending on the type of destination. Its (single) return value is the result of the conversion.</li> 1835 <li>A function: The function is called as <tt>o:__lanesconvert(string)</tt>, where the argument is either <tt>"keeper"</tt> (sending data in a Linda) or <tt>"regular"</tt> (when creating or calling a Lane generator). Its first return value is the result of the conversion, the rest is ignored. Transfer is then attempted on the new value. If this also requires a conversion, it will be done, up to a depth of 3, after which an error will be raised.</li>
1809 <li>Any other value raises an error.</li> 1836 <li>Any other value raises an error.</li>
1810 </ul> 1837 </ul>
1811 </li> 1838 </li>
diff --git a/src/intercopycontext.cpp b/src/intercopycontext.cpp
index 6e9b66c..a8b3374 100644
--- a/src/intercopycontext.cpp
+++ b/src/intercopycontext.cpp
@@ -513,72 +513,130 @@ void InterCopyContext::interCopyKeyValuePair() const
513 513
514// ################################################################################################# 514// #################################################################################################
515 515
516LuaType InterCopyContext::processConversion() const 516ConvertMode InterCopyContext::lookupConverter() const
517{ 517{
518 static constexpr int kPODmask = (1 << LUA_TNIL) | (1 << LUA_TBOOLEAN) | (1 << LUA_TLIGHTUSERDATA) | (1 << LUA_TNUMBER) | (1 << LUA_TSTRING); 518 static constexpr std::string_view kConvertField{ "__lanesconvert" };
519
520 LuaType _val_type{ luaW_type(L1, L1_i) };
521
522 STACK_CHECK_START_REL(L1, 0); 519 STACK_CHECK_START_REL(L1, 0);
523 520
524 // it's a POD: nothing to do 521 // lookup and push a converter on the stack
525 if (((1 << static_cast<int>(_val_type)) & kPODmask) != 0) { 522 LuaType const _convertType{
526 return _val_type; 523 std::invoke([this]() {
527 } 524 // never convert from inside a keeper
525 if (mode == LookupMode::FromKeeper) {
526 lua_pushnil(L1);
527 return LuaType::NIL;
528 }
529 if (lua_getmetatable(L1, L1_i)) { // L1: ... mt
530 // we have a metatable: grab the converter inside it
531 LuaType const _convertType{ luaW_getfield(L1, kIdxTop, kConvertField) }; // L1: ... mt <converter>
532 lua_remove(L1, -2); // L1: ... <converter>
533 return _convertType;
534 }
535 // no metatable: setup converter from the global settings
536 switch (U->convertMode) {
537 case ConvertMode::DoNothing:
538 lua_pushnil(L1);
539 return LuaType::NIL;
540
541 case ConvertMode::ConvertToNil:
542 kNilSentinel.pushKey(L1);
543 return LuaType::LIGHTUSERDATA;
544
545 case ConvertMode::Decay:
546 luaW_pushstring(L1, "decay");
547 return LuaType::STRING;
548
549 case ConvertMode::UserConversion:
550 raise_luaL_error(getErrL(), "INTERNAL ERROR: function-based conversion should have been prevented at configure settings validation");
551 }
552 // technically unreachable, since all cases are handled and raise_luaL_error() is [[noreturn]], but MSVC raises C4715 without that:
553 return LuaType::NIL;
554 })
555 };
556 STACK_CHECK(L1, 1);
528 557
529 // no metatable: nothing to do 558 ConvertMode _convertMode{ ConvertMode::DoNothing };
530 if (!lua_getmetatable(L1, L1_i)) { // L1: ... 559 LUA_ASSERT(getErrL(), luaW_type(L1, kIdxTop) == _convertType);
531 STACK_CHECK(L1, 0); 560 switch (_convertType) {
532 return _val_type; 561 case LuaType::NIL: // L1: ... nil
533 } 562 // no converter, nothing to do
534 // we have a metatable // L1: ... mt
535 static constexpr std::string_view kConvertField{ "__lanesconvert" };
536 LuaType const _converterType{ luaW_getfield(L1, kIdxTop, kConvertField) }; // L1: ... mt kConvertField
537 switch (_converterType) {
538 case LuaType::NIL:
539 // no __lanesconvert, nothing to do
540 lua_pop(L1, 2); // L1: ...
541 break; 563 break;
542 564
543 case LuaType::LIGHTUSERDATA: 565 case LuaType::LIGHTUSERDATA:
544 if (kNilSentinel.equals(L1, kIdxTop)) { 566 if (!kNilSentinel.equals(L1, kIdxTop)) {
545 DEBUGSPEW_CODE(DebugSpew(U) << "converted " << luaW_typename(L1, _val_type) << " to nil" << std::endl); 567 raise_luaL_error(getErrL(), "Invalid %s type %s", kConvertField.data(), luaW_typename(L1, _convertType).data());
546 lua_replace(L1, L1_i); // L1: ... mt
547 lua_pop(L1, 1); // L1: ...
548 _val_type = _converterType;
549 } else {
550 raise_luaL_error(getErrL(), "Invalid %s type %s", kConvertField.data(), luaW_typename(L1, _converterType).data());
551 } 568 }
569 _convertMode = ConvertMode::ConvertToNil;
552 break; 570 break;
553 571
554 case LuaType::STRING: 572 case LuaType::STRING:
555 // kConvertField == "decay" -> replace source value with it's pointer 573 if (std::string_view const _mode{ luaW_tostring(L1, kIdxTop) }; _mode != "decay") { // L1: ... "<some string>"
556 if (std::string_view const _mode{ luaW_tostring(L1, kIdxTop) }; _mode == "decay") {
557 lua_pop(L1, 1); // L1: ... mt
558 lua_pushlightuserdata(L1, const_cast<void*>(lua_topointer(L1, L1_i))); // L1: ... mt decayed
559 lua_replace(L1, L1_i); // L1: ... mt
560 lua_pop(L1, 1); // L1: ...
561 _val_type = LuaType::LIGHTUSERDATA;
562 } else {
563 raise_luaL_error(getErrL(), "Invalid %s mode '%s'", kConvertField.data(), _mode.data()); 574 raise_luaL_error(getErrL(), "Invalid %s mode '%s'", kConvertField.data(), _mode.data());
564 } 575 }
576 _convertMode = ConvertMode::Decay;
565 break; 577 break;
566 578
567 case LuaType::FUNCTION: 579 case LuaType::FUNCTION: // L1: ... <some_function>
568 lua_pushvalue(L1, L1_i); // L1: ... mt kConvertField val 580 _convertMode = ConvertMode::UserConversion;
569 luaW_pushstring(L1, mode == LookupMode::ToKeeper ? "keeper" : "regular"); // L1: ... mt kConvertField val string
570 lua_call(L1, 2, 1); // val:kConvertField(str) -> result // L1: ... mt kConvertField converted
571 lua_replace(L1, L1_i); // L1: ... mt
572 lua_pop(L1, 1); // L1: ... mt
573 _val_type = luaW_type(L1, L1_i);
574 break; 581 break;
575 582
576 default: 583 default:
577 raise_luaL_error(getErrL(), "Invalid %s type %s", kConvertField.data(), luaW_typename(L1, _converterType).data()); 584 raise_luaL_error(getErrL(), "Invalid %s type %s", kConvertField.data(), luaW_typename(L1, _convertType).data());
585 }
586 STACK_CHECK(L1, 1); // L1: ... <converter>
587 return _convertMode;
588}
589
590// #################################################################################################
591
592bool InterCopyContext::processConversion() const
593{
594#if HAVE_LUA_ASSERT() || USE_DEBUG_SPEW()
595 LuaType const _val_type{ luaW_type(L1, L1_i) };
596#endif // HAVE_LUA_ASSERT() || USE_DEBUG_SPEW()
597 LUA_ASSERT(getErrL(), _val_type == LuaType::TABLE || _val_type == LuaType::USERDATA);
598
599 STACK_CHECK_START_REL(L1, 0);
600
601 ConvertMode const _convertMode{ lookupConverter() }; // L1: ... <converter>
602 STACK_CHECK(L1, 1);
603
604 bool _converted{ true };
605 switch (_convertMode) {
606 case ConvertMode::DoNothing:
607 LUA_ASSERT(getErrL(), luaW_type(L1, kIdxTop) == LuaType::NIL); // L1: ... nil
608 lua_pop(L1, 1); // L1: ...
609 _converted = false;
610 break;
611
612 case ConvertMode::ConvertToNil:
613 LUA_ASSERT(getErrL(), kNilSentinel.equals(L1, kIdxTop)); // L1: ... kNilSentinel
614 DEBUGSPEW_CODE(DebugSpew(U) << "converted " << luaW_typename(L1, _val_type) << " to nil" << std::endl);
615 lua_replace(L1, L1_i); // L1: ...
616 break;
617
618 case ConvertMode::Decay:
619 // kConvertField == "decay" -> replace source value with its pointer
620 LUA_ASSERT(getErrL(), luaW_tostring(L1, kIdxTop) == "decay");
621 DEBUGSPEW_CODE(DebugSpew(U) << "converted " << luaW_typename(L1, _val_type) << " to a pointer" << std::endl);
622 lua_pop(L1, 1); // L1: ...
623 lua_pushlightuserdata(L1, const_cast<void*>(lua_topointer(L1, L1_i))); // L1: ... decayed
624 lua_replace(L1, L1_i); // L1: ...
625 break;
626
627 case ConvertMode::UserConversion:
628 lua_pushvalue(L1, L1_i); // L1: ... converter() val
629 luaW_pushstring(L1, mode == LookupMode::ToKeeper ? "keeper" : "regular"); // L1: ... converter() val string
630 lua_call(L1, 2, 1); // val:converter(str) -> result // L1: ... converted
631 lua_replace(L1, L1_i); // L1: ...
632 DEBUGSPEW_CODE(DebugSpew(U) << "converted " << luaW_typename(L1, _val_type) << " to a " << luaW_typename(L1, L1_i) << std::endl);
633 break;
634
635 default:
636 raise_luaL_error(getErrL(), "INTERNAL ERROR: SHOULD NEVER GET HERE");
578 } 637 }
579 STACK_CHECK(L1, 0); 638 STACK_CHECK(L1, 0);
580 LUA_ASSERT(getErrL(), luaW_type(L1, L1_i) == _val_type); 639 return _converted;
581 return _val_type;
582} 640}
583 641
584// ################################################################################################# 642// #################################################################################################
@@ -867,13 +925,11 @@ bool InterCopyContext::tryCopyDeep() const
867 925
868// ################################################################################################# 926// #################################################################################################
869 927
870[[nodiscard]] 928void InterCopyContext::interCopyBoolean() const
871bool InterCopyContext::interCopyBoolean() const
872{ 929{
873 int const _v{ lua_toboolean(L1, L1_i) }; 930 int const _v{ lua_toboolean(L1, L1_i) };
874 DEBUGSPEW_CODE(DebugSpew(nullptr) << (_v ? "true" : "false") << std::endl); 931 DEBUGSPEW_CODE(DebugSpew(nullptr) << (_v ? "true" : "false") << std::endl);
875 lua_pushboolean(L2, _v); 932 lua_pushboolean(L2, _v);
876 return true;
877} 933}
878 934
879// ################################################################################################# 935// #################################################################################################
@@ -972,8 +1028,7 @@ bool InterCopyContext::interCopyFunction() const
972 1028
973// ################################################################################################# 1029// #################################################################################################
974 1030
975[[nodiscard]] 1031void InterCopyContext::interCopyLightuserdata() const
976bool InterCopyContext::interCopyLightuserdata() const
977{ 1032{
978 void* const _p{ lua_touserdata(L1, L1_i) }; 1033 void* const _p{ lua_touserdata(L1, L1_i) };
979 // recognize and print known UniqueKey names here 1034 // recognize and print known UniqueKey names here
@@ -999,7 +1054,6 @@ bool InterCopyContext::interCopyLightuserdata() const
999 lua_pushlightuserdata(L2, _p); 1054 lua_pushlightuserdata(L2, _p);
1000 DEBUGSPEW_CODE(DebugSpew(nullptr) << std::endl); 1055 DEBUGSPEW_CODE(DebugSpew(nullptr) << std::endl);
1001 } 1056 }
1002 return true;
1003} 1057}
1004 1058
1005// ################################################################################################# 1059// #################################################################################################
@@ -1021,8 +1075,7 @@ bool InterCopyContext::interCopyNil() const
1021 1075
1022// ################################################################################################# 1076// #################################################################################################
1023 1077
1024[[nodiscard]] 1078void InterCopyContext::interCopyNumber() const
1025bool InterCopyContext::interCopyNumber() const
1026{ 1079{
1027 // LNUM patch support (keeping integer accuracy) 1080 // LNUM patch support (keeping integer accuracy)
1028#if defined LUA_LNUM || LUA_VERSION_NUM >= 503 1081#if defined LUA_LNUM || LUA_VERSION_NUM >= 503
@@ -1037,40 +1090,44 @@ bool InterCopyContext::interCopyNumber() const
1037 DEBUGSPEW_CODE(DebugSpew(nullptr) << _v << std::endl); 1090 DEBUGSPEW_CODE(DebugSpew(nullptr) << _v << std::endl);
1038 lua_pushnumber(L2, _v); 1091 lua_pushnumber(L2, _v);
1039 } 1092 }
1040 return true;
1041} 1093}
1042 1094
1043// ################################################################################################# 1095// #################################################################################################
1044 1096
1045[[nodiscard]] 1097void InterCopyContext::interCopyString() const
1046bool InterCopyContext::interCopyString() const
1047{ 1098{
1048 std::string_view const _s{ luaW_tostring(L1, L1_i) }; 1099 std::string_view const _s{ luaW_tostring(L1, L1_i) };
1049 DEBUGSPEW_CODE(DebugSpew(nullptr) << "'" << _s << "'" << std::endl); 1100 DEBUGSPEW_CODE(DebugSpew(nullptr) << "'" << _s << "'" << std::endl);
1050 luaW_pushstring(L2, _s); 1101 luaW_pushstring(L2, _s);
1051 return true;
1052} 1102}
1053 1103
1054// ################################################################################################# 1104// #################################################################################################
1055 1105
1056[[nodiscard]] 1106[[nodiscard]]
1057bool InterCopyContext::interCopyTable() const 1107InterCopyOneResult InterCopyContext::interCopyTable() const
1058{ 1108{
1059 if (vt == VT::KEY) { 1109 if (vt == VT::KEY) {
1060 return false; 1110 return InterCopyOneResult::NotCopied;
1061 } 1111 }
1062 1112
1063 STACK_CHECK_START_REL(L1, 0); 1113 STACK_CHECK_START_REL(L1, 0);
1064 STACK_CHECK_START_REL(L2, 0); 1114 STACK_CHECK_START_REL(L2, 0);
1065 DEBUGSPEW_CODE(DebugSpew(nullptr) << "TABLE " << name << std::endl); 1115 DEBUGSPEW_CODE(DebugSpew(nullptr) << "TABLE " << name << std::endl);
1066 1116
1117 // replace the value at L1_i with the result of a conversion if required
1118 bool const _converted{ processConversion() };
1119 if (_converted) {
1120 return InterCopyOneResult::RetryAfterConversion;
1121 }
1122 STACK_CHECK(L1, 0);
1123
1067 /* 1124 /*
1068 * First, let's try to see if this table is special (aka is it some table that we registered in our lookup databases during module registration?) 1125 * First, let's try to see if this table is special (aka is it some table that we registered in our lookup databases during module registration?)
1069 * Note that this table CAN be a module table, but we just didn't register it, in which case we'll send it through the table cloning mechanism 1126 * Note that this table CAN be a module table, but we just didn't register it, in which case we'll send it through the table cloning mechanism
1070 */ 1127 */
1071 if (lookupTable()) { 1128 if (lookupTable()) {
1072 LUA_ASSERT(L1, lua_istable(L2, -1) || (lua_tocfunction(L2, -1) == table_lookup_sentinel)); // from lookup data. can also be table_lookup_sentinel if this is a table we know 1129 LUA_ASSERT(L1, lua_istable(L2, -1) || (lua_tocfunction(L2, -1) == table_lookup_sentinel)); // from lookup data. can also be table_lookup_sentinel if this is a table we know
1073 return true; 1130 return InterCopyOneResult::Copied;
1074 } 1131 }
1075 1132
1076 /* Check if we've already copied the same table from 'L1' (during this transmission), and 1133 /* Check if we've already copied the same table from 'L1' (during this transmission), and
@@ -1084,7 +1141,7 @@ bool InterCopyContext::interCopyTable() const
1084 */ 1141 */
1085 if (pushCachedTable()) { // L2: ... t 1142 if (pushCachedTable()) { // L2: ... t
1086 LUA_ASSERT(L1, lua_istable(L2, -1)); // from cache 1143 LUA_ASSERT(L1, lua_istable(L2, -1)); // from cache
1087 return true; 1144 return InterCopyOneResult::Copied;
1088 } 1145 }
1089 LUA_ASSERT(L1, lua_istable(L2, -1)); 1146 LUA_ASSERT(L1, lua_istable(L2, -1));
1090 1147
@@ -1106,25 +1163,25 @@ bool InterCopyContext::interCopyTable() const
1106 } 1163 }
1107 STACK_CHECK(L2, 1); 1164 STACK_CHECK(L2, 1);
1108 STACK_CHECK(L1, 0); 1165 STACK_CHECK(L1, 0);
1109 return true; 1166 return InterCopyOneResult::Copied;
1110} 1167}
1111 1168
1112// ################################################################################################# 1169// #################################################################################################
1113 1170
1114[[nodiscard]] 1171[[nodiscard]]
1115bool InterCopyContext::interCopyUserdata() const 1172InterCopyOneResult InterCopyContext::interCopyUserdata() const
1116{ 1173{
1117 STACK_CHECK_START_REL(L1, 0); 1174 STACK_CHECK_START_REL(L1, 0);
1118 STACK_CHECK_START_REL(L2, 0); 1175 STACK_CHECK_START_REL(L2, 0);
1119 if (vt == VT::KEY) { 1176 if (vt == VT::KEY) {
1120 return false; 1177 return InterCopyOneResult::NotCopied;
1121 } 1178 }
1122 1179
1123 // try clonable userdata first 1180 // try clonable userdata first
1124 if (tryCopyClonable()) { 1181 if (tryCopyClonable()) {
1125 STACK_CHECK(L1, 0); 1182 STACK_CHECK(L1, 0);
1126 STACK_CHECK(L2, 1); 1183 STACK_CHECK(L2, 1);
1127 return true; 1184 return InterCopyOneResult::Copied;
1128 } 1185 }
1129 1186
1130 STACK_CHECK(L1, 0); 1187 STACK_CHECK(L1, 0);
@@ -1134,13 +1191,20 @@ bool InterCopyContext::interCopyUserdata() const
1134 if (tryCopyDeep()) { 1191 if (tryCopyDeep()) {
1135 STACK_CHECK(L1, 0); 1192 STACK_CHECK(L1, 0);
1136 STACK_CHECK(L2, 1); 1193 STACK_CHECK(L2, 1);
1137 return true; 1194 return InterCopyOneResult::Copied;
1195 }
1196
1197 // replace the value at L1_i with the result of a conversion if required
1198 bool const _converted{ processConversion() };
1199 if (_converted) {
1200 return InterCopyOneResult::RetryAfterConversion;
1138 } 1201 }
1202 STACK_CHECK(L1, 0);
1139 1203
1140 // Last, let's try to see if this userdata is special (aka is it some userdata that we registered in our lookup databases during module registration?) 1204 // Last, let's try to see if this userdata is special (aka is it some userdata that we registered in our lookup databases during module registration?)
1141 if (lookupUserdata()) { 1205 if (lookupUserdata()) {
1142 LUA_ASSERT(L1, luaW_type(L2, kIdxTop) == LuaType::USERDATA || (lua_tocfunction(L2, kIdxTop) == userdata_lookup_sentinel)); // from lookup data. can also be userdata_lookup_sentinel if this is a userdata we know 1206 LUA_ASSERT(L1, luaW_type(L2, kIdxTop) == LuaType::USERDATA || (lua_tocfunction(L2, kIdxTop) == userdata_lookup_sentinel)); // from lookup data. can also be userdata_lookup_sentinel if this is a userdata we know
1143 return true; 1207 return InterCopyOneResult::Copied;
1144 } 1208 }
1145 1209
1146 raise_luaL_error(getErrL(), "can't copy non-deep full userdata across lanes"); 1210 raise_luaL_error(getErrL(), "can't copy non-deep full userdata across lanes");
@@ -1193,54 +1257,64 @@ InterCopyResult InterCopyContext::interCopyOne() const
1193 DEBUGSPEW_CODE(DebugSpew(U) << "interCopyOne()" << std::endl); 1257 DEBUGSPEW_CODE(DebugSpew(U) << "interCopyOne()" << std::endl);
1194 DEBUGSPEW_CODE(DebugSpewIndentScope _scope{ U }); 1258 DEBUGSPEW_CODE(DebugSpewIndentScope _scope{ U });
1195 1259
1196 // replace the value at L1_i with the result of a conversion if required 1260 DEBUGSPEW_CODE(DebugSpew(U) << local::sLuaTypeNames[static_cast<int>(luaW_type(L1, L1_i))] << " " << local::sValueTypeNames[static_cast<int>(vt)] << ": ");
1197 LuaType const _val_type{ processConversion() };
1198 STACK_CHECK(L1, 0);
1199 DEBUGSPEW_CODE(DebugSpew(U) << local::sLuaTypeNames[static_cast<int>(_val_type)] << " " << local::sValueTypeNames[static_cast<int>(vt)] << ": ");
1200
1201 // Lets push nil to L2 if the object should be ignored
1202 bool _ret{ true };
1203 switch (_val_type) {
1204 // Basic types allowed both as values, and as table keys
1205 case LuaType::BOOLEAN:
1206 _ret = interCopyBoolean();
1207 break;
1208 case LuaType::NUMBER:
1209 _ret = interCopyNumber();
1210 break;
1211 case LuaType::STRING:
1212 _ret = interCopyString();
1213 break;
1214 case LuaType::LIGHTUSERDATA:
1215 _ret = interCopyLightuserdata();
1216 break;
1217 1261
1218 // The following types are not allowed as table keys 1262 auto _tryCopy = [this]() {
1219 case LuaType::USERDATA: 1263 LuaType const _val_type{ luaW_type(L1, L1_i) };
1220 _ret = interCopyUserdata(); 1264 InterCopyOneResult _result{ InterCopyOneResult::Copied };
1221 break; 1265 switch (_val_type) {
1222 case LuaType::NIL: 1266 // Basic types allowed both as values, and as table keys
1223 _ret = interCopyNil(); 1267 case LuaType::BOOLEAN:
1224 break; 1268 interCopyBoolean();
1225 case LuaType::FUNCTION: 1269 break;
1226 _ret = interCopyFunction(); 1270 case LuaType::NUMBER:
1227 break; 1271 interCopyNumber();
1228 case LuaType::TABLE: 1272 break;
1229 _ret = interCopyTable(); 1273 case LuaType::STRING:
1230 break; 1274 interCopyString();
1275 break;
1276 case LuaType::LIGHTUSERDATA:
1277 interCopyLightuserdata();
1278 break;
1231 1279
1232 // The following types cannot be copied 1280 // The following types are not allowed as table keys
1233 case LuaType::NONE: 1281 case LuaType::USERDATA:
1234 case LuaType::CDATA: 1282 _result = interCopyUserdata();
1235 [[fallthrough]]; 1283 break;
1236 case LuaType::THREAD: 1284 case LuaType::NIL:
1237 _ret = false; 1285 _result = interCopyNil() ? InterCopyOneResult::Copied : InterCopyOneResult::NotCopied;
1238 break; 1286 break;
1239 } 1287 case LuaType::FUNCTION:
1288 _result = interCopyFunction() ? InterCopyOneResult::Copied : InterCopyOneResult::NotCopied;
1289 break;
1290 case LuaType::TABLE:
1291 _result = interCopyTable();
1292 break;
1293
1294 // The following types cannot be copied
1295 case LuaType::NONE:
1296 case LuaType::CDATA:
1297 [[fallthrough]];
1298
1299 case LuaType::THREAD:
1300 _result = InterCopyOneResult::NotCopied;
1301 break;
1302 }
1303 return _result;
1304 };
1305
1306 uint32_t _conversionCount{ 0 };
1307 InterCopyOneResult _result{ InterCopyOneResult::Copied };
1308 do {
1309 if (_conversionCount++ > U->convertMaxAttempts) {
1310 raise_luaL_error(getErrL(), "more than %d conversion attempts", U->convertMaxAttempts);
1311 }
1312 _result = _tryCopy();
1313 } while (_result == InterCopyOneResult::RetryAfterConversion);
1240 1314
1241 STACK_CHECK(L2, _ret ? 1 : 0); 1315 STACK_CHECK(L2, (_result == InterCopyOneResult::Copied) ? 1 : 0);
1242 STACK_CHECK(L1, 0); 1316 STACK_CHECK(L1, 0);
1243 return _ret ? InterCopyResult::Success : InterCopyResult::Error; 1317 return (_result == InterCopyOneResult::Copied) ? InterCopyResult::Success : InterCopyResult::Error;
1244} 1318}
1245 1319
1246// ################################################################################################# 1320// #################################################################################################
diff --git a/src/intercopycontext.hpp b/src/intercopycontext.hpp
index ece4674..0b88666 100644
--- a/src/intercopycontext.hpp
+++ b/src/intercopycontext.hpp
@@ -1,6 +1,7 @@
1#pragma once 1#pragma once
2 2
3#include "tools.hpp" 3#include "tools.hpp"
4#include "universe.hpp"
4 5
5// forwards 6// forwards
6class Universe; 7class Universe;
@@ -21,6 +22,13 @@ enum class [[nodiscard]] InterCopyResult
21 Error 22 Error
22}; 23};
23 24
25enum class [[nodiscard]] InterCopyOneResult
26{
27 NotCopied,
28 Copied,
29 RetryAfterConversion
30};
31
24// ################################################################################################# 32// #################################################################################################
25 33
26DECLARE_UNIQUE_TYPE(CacheIndex, StackIndex); 34DECLARE_UNIQUE_TYPE(CacheIndex, StackIndex);
@@ -43,8 +51,14 @@ class InterCopyContext final
43 // when mode == LookupMode::FromKeeper, L1 is a keeper state and L2 is not, therefore L2 is the state where we want to raise the error 51 // when mode == LookupMode::FromKeeper, L1 is a keeper state and L2 is not, therefore L2 is the state where we want to raise the error
44 // whon mode != LookupMode::FromKeeper, L1 is not a keeper state, therefore L1 is the state where we want to raise the error 52 // whon mode != LookupMode::FromKeeper, L1 is not a keeper state, therefore L1 is the state where we want to raise the error
45 lua_State* getErrL() const { return (mode == LookupMode::FromKeeper) ? L2.value() : L1.value(); } 53 lua_State* getErrL() const { return (mode == LookupMode::FromKeeper) ? L2.value() : L1.value(); }
54
55 // for use in processConversion
56 [[nodiscard]]
57 ConvertMode lookupConverter() const;
58
59 // for use in interCopyTable and interCopyUserdata
46 [[nodiscard]] 60 [[nodiscard]]
47 LuaType processConversion() const; 61 bool processConversion() const;
48 62
49 // for use in copyCachedFunction 63 // for use in copyCachedFunction
50 void copyFunction() const; 64 void copyFunction() const;
@@ -71,22 +85,18 @@ class InterCopyContext final
71 bool tryCopyDeep() const; 85 bool tryCopyDeep() const;
72 86
73 // copying a single Lua stack item 87 // copying a single Lua stack item
74 [[nodiscard]] 88 void interCopyBoolean() const;
75 bool interCopyBoolean() const;
76 [[nodiscard]] 89 [[nodiscard]]
77 bool interCopyFunction() const; 90 bool interCopyFunction() const;
78 [[nodiscard]] 91 void interCopyLightuserdata() const;
79 bool interCopyLightuserdata() const;
80 [[nodiscard]] 92 [[nodiscard]]
81 bool interCopyNil() const; 93 bool interCopyNil() const;
94 void interCopyNumber() const;
95 void interCopyString() const;
82 [[nodiscard]] 96 [[nodiscard]]
83 bool interCopyNumber() const; 97 InterCopyOneResult interCopyTable() const;
84 [[nodiscard]]
85 bool interCopyString() const;
86 [[nodiscard]]
87 bool interCopyTable() const;
88 [[nodiscard]] 98 [[nodiscard]]
89 bool interCopyUserdata() const; 99 InterCopyOneResult interCopyUserdata() const;
90 100
91 public: 101 public:
92 [[nodiscard]] 102 [[nodiscard]]
diff --git a/src/lanes.cpp b/src/lanes.cpp
index 4373aee..024ac67 100644
--- a/src/lanes.cpp
+++ b/src/lanes.cpp
@@ -907,6 +907,9 @@ LANES_API int luaopen_lanes_core(lua_State* const L_)
907 // will do nothing on first invocation, as we haven't stored settings in the registry yet 907 // will do nothing on first invocation, as we haven't stored settings in the registry yet
908 lua_setfield(L_, -3, "settings"); // L_: M LG_configure() 908 lua_setfield(L_, -3, "settings"); // L_: M LG_configure()
909 lua_setfield(L_, -2, "configure"); // L_: M 909 lua_setfield(L_, -2, "configure"); // L_: M
910 // lanes.null can be used for some configure settings, expose it now
911 kNilSentinel.pushKey(L_); // L_: M kNilSentinel
912 lua_setfield(L_, -2, "null"); // L_: M
910 } 913 }
911 914
912 STACK_CHECK(L_, 1); 915 STACK_CHECK(L_, 1);
diff --git a/src/lanes.lua b/src/lanes.lua
index c5b3315..43ebfd5 100644
--- a/src/lanes.lua
+++ b/src/lanes.lua
@@ -93,6 +93,8 @@ local default_params =
93{ 93{
94 -- LuaJIT provides a thread-unsafe allocator by default, so we need to protect it when used in parallel lanes 94 -- LuaJIT provides a thread-unsafe allocator by default, so we need to protect it when used in parallel lanes
95 allocator = isLuaJIT and "protected" or nil, 95 allocator = isLuaJIT and "protected" or nil,
96 convert_fallback = nil,
97 convert_max_attempts = 1,
96 -- it looks also like LuaJIT allocator may not appreciate direct use of its allocator for other purposes than the VM operation 98 -- it looks also like LuaJIT allocator may not appreciate direct use of its allocator for other purposes than the VM operation
97 internal_allocator = isLuaJIT and "libc" or "allocator", 99 internal_allocator = isLuaJIT and "libc" or "allocator",
98 keepers_gc_threshold = -1, 100 keepers_gc_threshold = -1,
@@ -125,6 +127,23 @@ local param_checkers =
125 end 127 end
126 return true 128 return true
127 end, 129 end,
130 convert_fallback = function(val_)
131 -- convert_fallback should be nil, lanes.null or 'decay'
132 if val_ == nil or val_ == core.null or val_ == 'decay' then
133 return true
134 end
135 return nil, "must be nil, lanes.null or 'decay'"
136 end, -- convert_fallback
137 convert_max_attempts = function(val_)
138 -- convert_max_attempts should be a number
139 if type(val_) ~= "number" then
140 return nil, "not a number"
141 end
142 if val_ <= 0 or val_ >= 10 then
143 return nil, "value out of range"
144 end
145 return true
146 end, -- convert_fallback
128 internal_allocator = function(val_) 147 internal_allocator = function(val_)
129 -- can be "libc" or "allocator" 148 -- can be "libc" or "allocator"
130 if type(val_) ~= "string" then 149 if type(val_) ~= "string" then
@@ -850,12 +869,8 @@ local configure = function(settings_)
850 lanes.configure = function() return lanes end -- no need to configure anything again 869 lanes.configure = function() return lanes end -- no need to configure anything again
851 870
852 -- now we can configure Lanes core 871 -- now we can configure Lanes core
853
854
855
856
857
858 local settings = core.configure and core.configure(params_checker(settings_)) or core.settings 872 local settings = core.configure and core.configure(params_checker(settings_)) or core.settings
873 assert(type(settings) == 'table')
859 874
860 -- 875 --
861 lanes.ABOUT = 876 lanes.ABOUT =
@@ -912,7 +927,10 @@ lanesMeta.__index = function(lanes_, k_)
912 -- Access the required key 927 -- Access the required key
913 return lanes_[k_] 928 return lanes_[k_]
914end 929end
930
915lanes.configure = configure 931lanes.configure = configure
932-- lanes.null can be used for some configure settings, expose it now
933lanes.null = assert(core.null)
916setmetatable(lanes, lanesMeta) 934setmetatable(lanes, lanesMeta)
917 935
918-- ################################################################################################# 936-- #################################################################################################
diff --git a/src/universe.cpp b/src/universe.cpp
index 4db036b..934db2c 100644
--- a/src/universe.cpp
+++ b/src/universe.cpp
@@ -163,6 +163,22 @@ Universe* Universe::Create(lua_State* const L_)
163 lua_setmetatable(L_, -2); // L_: settings universe 163 lua_setmetatable(L_, -2); // L_: settings universe
164 lua_pop(L_, 1); // L_: settings 164 lua_pop(L_, 1); // L_: settings
165 165
166 std::ignore = luaW_getfield(L_, kIdxSettings, "convert_fallback"); // L_: settings convert_fallback
167 if (kNilSentinel.equals(L_, kIdxTop)) {
168 _U->convertMode = ConvertMode::ConvertToNil;
169 } else if (luaW_type(L_, kIdxTop) == LuaType::STRING) {
170 LUA_ASSERT(L_, luaW_tostring(L_, kIdxTop) == "decay");
171 _U->convertMode = ConvertMode::Decay;
172 } else {
173 LUA_ASSERT(L_, lua_isnil(L_, kIdxTop));
174 }
175 lua_pop(L_, 1); // L_: settings
176
177 std::ignore = luaW_getfield(L_, kIdxSettings, "convert_max_attempts"); // L_: settings convert_max_attempts
178 _U->convertMaxAttempts = static_cast<decltype(_U->convertMaxAttempts)>(lua_tointeger(L_, kIdxTop));
179 lua_pop(L_, 1); // L_: settings
180 STACK_CHECK(L_, 0);
181
166 std::ignore = luaW_getfield(L_, kIdxSettings, "linda_wake_period"); // L_: settings linda_wake_period 182 std::ignore = luaW_getfield(L_, kIdxSettings, "linda_wake_period"); // L_: settings linda_wake_period
167 if (luaW_type(L_, kIdxTop) == LuaType::NUMBER) { 183 if (luaW_type(L_, kIdxTop) == LuaType::NUMBER) {
168 _U->lindaWakePeriod = lua_Duration{ lua_tonumber(L_, kIdxTop) }; 184 _U->lindaWakePeriod = lua_Duration{ lua_tonumber(L_, kIdxTop) };
diff --git a/src/universe.hpp b/src/universe.hpp
index f781e92..1f3ffaf 100644
--- a/src/universe.hpp
+++ b/src/universe.hpp
@@ -17,6 +17,17 @@ enum class LookupMode;
17 17
18// ################################################################################################# 18// #################################################################################################
19 19
20// having this enum declared here is not ideal, but it will do for now
21enum class [[nodiscard]] ConvertMode
22{
23 DoNothing, // no conversion
24 ConvertToNil, // value is converted to nil
25 Decay, // value is converted to a light userdata pointer
26 UserConversion // value is converted by calling user-provided function
27};
28
29// #################################################################################################
30
20// mutex-protected allocator for use with Lua states that share a non-threadsafe allocator 31// mutex-protected allocator for use with Lua states that share a non-threadsafe allocator
21class ProtectedAllocator final 32class ProtectedAllocator final
22: public lanes::AllocatorDefinition 33: public lanes::AllocatorDefinition
@@ -95,6 +106,9 @@ class Universe final
95 106
96 Keepers keepers; 107 Keepers keepers;
97 108
109 ConvertMode convertMode{ ConvertMode::DoNothing };
110 uint32_t convertMaxAttempts{ 1 };
111
98 lua_Duration lindaWakePeriod{}; 112 lua_Duration lindaWakePeriod{};
99 113
100 // Initialized by 'init_once_LOCKED()': the deep userdata Linda object 114 // Initialized by 'init_once_LOCKED()': the deep userdata Linda object
diff --git a/unit_tests/UnitTests.vcxproj b/unit_tests/UnitTests.vcxproj
index 2093063..d5edef9 100644
--- a/unit_tests/UnitTests.vcxproj
+++ b/unit_tests/UnitTests.vcxproj
@@ -1106,6 +1106,7 @@
1106 <ClCompile Include="lane_tests.cpp" /> 1106 <ClCompile Include="lane_tests.cpp" />
1107 <ClCompile Include="legacy_tests.cpp" /> 1107 <ClCompile Include="legacy_tests.cpp" />
1108 <ClCompile Include="linda_tests.cpp" /> 1108 <ClCompile Include="linda_tests.cpp" />
1109 <ClCompile Include="misc_tests.cpp" />
1109 <ClCompile Include="shared.cpp" /> 1110 <ClCompile Include="shared.cpp" />
1110 <ClCompile Include="init_and_shutdown.cpp" /> 1111 <ClCompile Include="init_and_shutdown.cpp" />
1111 <ClCompile Include="_pch.cpp"> 1112 <ClCompile Include="_pch.cpp">
diff --git a/unit_tests/UnitTests.vcxproj.filters b/unit_tests/UnitTests.vcxproj.filters
index df82447..5d2dad8 100644
--- a/unit_tests/UnitTests.vcxproj.filters
+++ b/unit_tests/UnitTests.vcxproj.filters
@@ -18,6 +18,7 @@
18 <ClCompile Include="catch_amalgamated.cpp"> 18 <ClCompile Include="catch_amalgamated.cpp">
19 <Filter>Catch2</Filter> 19 <Filter>Catch2</Filter>
20 </ClCompile> 20 </ClCompile>
21 <ClCompile Include="misc_tests.cpp" />
21 </ItemGroup> 22 </ItemGroup>
22 <ItemGroup> 23 <ItemGroup>
23 <ClInclude Include="_pch.hpp" /> 24 <ClInclude Include="_pch.hpp" />
diff --git a/unit_tests/init_and_shutdown.cpp b/unit_tests/init_and_shutdown.cpp
index 69e4f1b..e7e99a0 100644
--- a/unit_tests/init_and_shutdown.cpp
+++ b/unit_tests/init_and_shutdown.cpp
@@ -205,6 +205,141 @@ TEST_CASE(("lanes.configure.allocator/protected"))
205 L.requireSuccess("require 'lanes'.configure{allocator = 'protected'}"); 205 L.requireSuccess("require 'lanes'.configure{allocator = 'protected'}");
206} 206}
207 207
208// #################################################################################################
209
210TEST_CASE("lanes.configure.convert_fallback")
211{
212 LuaState L{ LuaState::WithBaseLibs{ true }, LuaState::WithFixture{ true } };
213
214 SECTION("convert_fallback = <light userdata>")
215 {
216 SECTION("bad value")
217 {
218 // make sure our fixture works
219 L.requireSuccess("fixture = require 'fixture'; assert(type(fixture.newlightuserdata() == 'userdata'))");
220 // and that providing a bad userdata fails
221 L.requireFailure("lanes = require 'lanes'; lanes.configure{convert_fallback = fixture.newlightuserdata()}");
222 }
223
224 SECTION("lanes.null")
225 {
226 // lanes.null should be accessible even if we don't lanes.configure() immediately
227 L.requireSuccess("lanes = require 'lanes'; lanes_null = lanes.null; assert(type(lanes_null) == 'userdata')");
228 // and providing it should work fine
229 L.requireSuccess("lanes.configure{convert_fallback = lanes_null}");
230 }
231 }
232
233 // ---------------------------------------------------------------------------------------------
234
235 SECTION("convert_fallback = <string>")
236 {
237 SECTION("bad value")
238 {
239 L.requireFailure("require 'lanes'.configure{convert_fallback = 'some other string'}");
240 }
241 SECTION("'decay'")
242 {
243 L.requireSuccess("require 'lanes'.configure{convert_fallback = 'decay'}");
244 }
245 }
246
247 // ---------------------------------------------------------------------------------------------
248
249 SECTION("convert_fallback = <function>")
250 {
251 // cannot use functions in the global settings, like we can in __lanesconvert metatable entries
252 L.requireFailure("require 'lanes'.configure{convert_fallback = print}");
253 }
254
255 // ---------------------------------------------------------------------------------------------
256
257 SECTION("convert_fallback = <number>")
258 {
259 L.requireFailure("require 'lanes'.configure{convert_fallback = 42}");
260 }
261
262 // ---------------------------------------------------------------------------------------------
263
264 SECTION("convert_fallback = <boolean>")
265 {
266 L.requireFailure("require 'lanes'.configure{convert_fallback = true}");
267 }
268}
269
270// #################################################################################################
271
272TEST_CASE("lanes.configure.convert_max_attempts")
273{
274 LuaState L{ LuaState::WithBaseLibs{ true }, LuaState::WithFixture{ true } };
275
276 // ---------------------------------------------------------------------------------------------
277
278 SECTION("convert_max_attempts = <boolean>")
279 {
280 L.requireFailure("require 'lanes'.configure{convert_max_attempts = true}");
281 }
282
283 // ---------------------------------------------------------------------------------------------
284
285 SECTION("convert_max_attempts = <string>")
286 {
287 L.requireFailure("require 'lanes'.configure{convert_max_attempts = 'bob'}");
288 }
289
290 // ---------------------------------------------------------------------------------------------
291
292 SECTION("convert_max_attempts = <function>")
293 {
294 L.requireFailure("require 'lanes'.configure{convert_max_attempts = print}");
295 }
296
297 // ---------------------------------------------------------------------------------------------
298
299 SECTION("convert_max_attempts = <light userdata>")
300 {
301 // make sure our fixture works
302 L.requireSuccess("fixture = require 'fixture'; assert(type(fixture.newlightuserdata() == 'userdata'))");
303 L.requireFailure("require 'lanes'.configure{convert_max_attempts = fixture.newlightuserdata()}");
304 }
305
306 // ---------------------------------------------------------------------------------------------
307
308 SECTION("convert_max_attempts = <full userdata>")
309 {
310 // make sure our fixture works
311 L.requireSuccess("fixture = require 'fixture'; assert(type(fixture.newuserdata() == 'userdata'))");
312 L.requireFailure("require 'lanes'.configure{convert_max_attempts = fixture.newuserdata()}");
313 }
314
315 // ---------------------------------------------------------------------------------------------
316
317 SECTION("convert_max_attempts = 0")
318 {
319 L.requireFailure("require 'lanes'.configure{convert_max_attempts = 0}");
320 }
321
322 // ---------------------------------------------------------------------------------------------
323
324 SECTION("convert_max_attempts = 1")
325 {
326 L.requireSuccess("require 'lanes'.configure{convert_max_attempts = 1}");
327 }
328
329 // ---------------------------------------------------------------------------------------------
330
331 SECTION("convert_max_attempts = 9")
332 {
333 L.requireSuccess("require 'lanes'.configure{convert_max_attempts = 9}");
334 }
335
336 // ---------------------------------------------------------------------------------------------
337
338 SECTION("convert_max_attempts = 9")
339 {
340 L.requireFailure("require 'lanes'.configure{convert_max_attempts = 10}");
341 }
342}
208 343
209// ################################################################################################# 344// #################################################################################################
210 345
@@ -478,6 +613,32 @@ TEST_CASE("lanes.configure.on_state_create/configuration")
478 613
479// ################################################################################################# 614// #################################################################################################
480 615
616TEST_CASE("lanes.configure.returns_lanes")
617{
618 LuaState L{ LuaState::WithBaseLibs{ true }, LuaState::WithFixture{ false } };
619
620 // when lanes is required, it should contain a 'configure' function and a 'null' userdata
621 L.requireSuccess("lanes = require 'lanes'; lanes_configure1 = lanes.configure; assert(type(lanes.configure) == 'function')");
622 L.requireSuccess("assert(type(lanes.null) == 'userdata')");
623
624 // make sure we didn't automatically call lanes.configure() when indexing lanes to retrieve lanes.null
625 L.requireSuccess("lanes_configure2 = lanes.configure; assert(lanes_configure2 == lanes_configure1)");
626
627 // calling lanes.configure() should return lanes
628 L.requireSuccess("assert(lanes.configure() == lanes)");
629
630 // and the configure function should have been changed to another function
631 L.requireSuccess("lanes_configure3 = lanes.configure; assert(lanes_configure3 ~= lanes_configure2)");
632
633 // which returns lanes too
634 L.requireSuccess("assert(lanes.configure() == lanes)");
635
636 // and the configure function should not change again
637 L.requireSuccess("assert(lanes.configure == lanes_configure3)");
638}
639
640// #################################################################################################
641
481TEST_CASE("lanes.configure.shutdown_timeout") 642TEST_CASE("lanes.configure.shutdown_timeout")
482{ 643{
483 LuaState L{ LuaState::WithBaseLibs{ true }, LuaState::WithFixture{ false } }; 644 LuaState L{ LuaState::WithBaseLibs{ true }, LuaState::WithFixture{ false } };
diff --git a/unit_tests/misc_tests.cpp b/unit_tests/misc_tests.cpp
new file mode 100644
index 0000000..a2199aa
--- /dev/null
+++ b/unit_tests/misc_tests.cpp
@@ -0,0 +1,193 @@
1#include "_pch.hpp"
2
3#include "shared.h"
4
5// #################################################################################################
6// #################################################################################################
7
8TEST_CASE("misc.__lanesconvert.for_tables")
9{
10 LuaState S{ LuaState::WithBaseLibs{ true }, LuaState::WithFixture{ true } };
11 S.requireSuccess("lanes = require 'lanes'.configure()");
12
13 S.requireSuccess(
14 " l = lanes.linda()"
15 " t = setmetatable({}, {__lanesconvert = lanes.null})" // table with a nil-converter
16 " l:send('k', t)" // send the table
17 " key, out = l:receive('k')" // read it back
18 " assert(key == 'k' and type(out) == 'nil', 'got ' .. key .. ' ' .. tostring(out))" // should have become nil
19 );
20
21 S.requireSuccess(
22 " l = lanes.linda()"
23 " t = setmetatable({}, {__lanesconvert = 'decay'})" // table with a decay-converter
24 " l:send('k', t)" // send the table
25 " key, out = l:receive('k')" // read it back
26 " assert(key == 'k' and type(out) == 'userdata', 'got ' .. key .. ' ' .. tostring(out))" // should have become a light userdata
27 );
28
29 S.requireSuccess(
30 " l = lanes.linda()"
31 " t = setmetatable({}, {__lanesconvert = function(t, hint) return 'keeper' end})" // table with a string-converter
32 " l:send('k', t)" // send the table
33 " key, out = l:receive('k')" // read it back
34 " assert(key == 'k' and out == 'keeper')" // should become 'keeper', the hint that the function received
35 );
36
37 // make sure that a function that returns the original object causes an error (we don't want infinite loops during conversion)
38 S.requireFailure(
39 " l = lanes.linda()"
40 " t = setmetatable({}, {__lanesconvert = function(t, hint) return t end})" // table with a string-converter
41 " l:send('k', t)" // send the table, it should raise an error because the converter triggers an infinite loop
42 );
43}
44
45// #################################################################################################
46// #################################################################################################
47
48TEST_CASE("misc.__lanesconvert.for_userdata")
49{
50 LuaState S{ LuaState::WithBaseLibs{ true }, LuaState::WithFixture{ true } };
51 S.requireSuccess("lanes = require 'lanes'.configure()");
52 S.requireSuccess("fixture = require 'fixture'");
53
54 S.requireSuccess("u_tonil = fixture.newuserdata{__lanesconvert = lanes.null}; assert(type(u_tonil) == 'userdata')");
55 S.requireSuccess(
56 " l = lanes.linda()"
57 " l:send('k', u_tonil)" // send a full userdata with a nil-converter
58 " key, out = l:receive('k')" // read it back
59 " assert(key == 'k' and type(out) == 'nil')" // should become nil
60 );
61
62 S.requireSuccess("u_tolud = fixture.newuserdata{__lanesconvert = 'decay'}; assert(type(u_tolud) == 'userdata')");
63 S.requireSuccess(
64 " l = lanes.linda()"
65 " l:send('k', u_tolud)" // send a full userdata with a decay-converter
66 " key, out = l:receive('k')" // read it back
67 " assert(key == 'k' and type(out) == 'userdata' and getmetatable(out) == nil)" // should become a light userdata
68 );
69
70 S.requireSuccess("u_tostr = fixture.newuserdata{__lanesconvert = function() return 'yo' end}; assert(type(u_tostr) == 'userdata')");
71 S.requireSuccess(
72 " l = lanes.linda()"
73 " l:send('k', u_tostr)" // send a full userdata with a string-converter
74 " key, out = l:receive('k')" // read it back
75 " assert(key == 'k' and out == 'yo')" // should become 'yo'
76 );
77
78 // make sure that a function that returns the original object causes an error (we don't want infinite loops during conversion)
79 S.requireSuccess("u_toself = fixture.newuserdata{__lanesconvert = function(u) return u end}; assert(type(u_toself) == 'userdata')");
80 S.requireFailure(
81 " l = lanes.linda()"
82 " l:send('k', u_toself)" // send the userdata, it should raise an error because the converter triggers an infinite loop
83 );
84
85 // TODO: make sure that a deep userdata with a __lanesconvert isn't converted (because deep copy takes precedence)
86}
87
88// #################################################################################################
89// #################################################################################################
90
91TEST_CASE("misc.convert_fallback.unset")
92{
93 LuaState S{ LuaState::WithBaseLibs{ true }, LuaState::WithFixture{ true } };
94 S.requireSuccess("lanes = require 'lanes'.configure()");
95
96 S.requireSuccess(
97 " l = lanes.linda()"
98 " l:send('k', {})" // send a table without a metatable
99 " key, out = l:receive('k')" // read it back
100 " assert(key == 'k' and type(out) == 'table')" // should not change
101 );
102
103 S.requireSuccess("fixture = require 'fixture'; u = fixture.newuserdata(); assert(type(u) == 'userdata')");
104 S.requireFailure(
105 " l = lanes.linda()"
106 " l:send('k', u)" // send a full userdata without a metatable, should fail
107 );
108}
109
110// #################################################################################################
111// #################################################################################################
112
113TEST_CASE("misc.convert_fallback.decay")
114{
115 LuaState S{ LuaState::WithBaseLibs{ true }, LuaState::WithFixture{ true } };
116 S.requireSuccess("lanes = require 'lanes'.configure{convert_fallback = 'decay'}");
117 S.requireSuccess("fixture = require 'fixture'");
118
119 S.requireSuccess(
120 " l = lanes.linda()"
121 " l:send('k', {})" // send a table without a metatable
122 " key, out = l:receive('k')" // read it back
123 " assert(key == 'k' and type(out) == 'userdata' and getmetatable(out) == nil)" // should have become a light userdata
124 );
125
126 S.requireSuccess("u = fixture.newuserdata(); assert(type(u) == 'userdata')");
127 S.requireSuccess(
128 " l = lanes.linda()"
129 " l:send('k', u)" // send a non-copyable non-deep full userdata
130 " key, out = l:receive('k')" // read it back
131 " assert(key == 'k' and type(out) == 'userdata' and getmetatable(out) == nil)" // should have become a light userdata
132 );
133}
134
135// #################################################################################################
136// #################################################################################################
137
138TEST_CASE("misc.convert_fallback.convert_no_nil")
139{
140 LuaState S{ LuaState::WithBaseLibs{ true }, LuaState::WithFixture{ false } };
141 S.requireSuccess("lanes = require 'lanes'; lanes.configure{convert_fallback = lanes.null}");
142
143 S.requireSuccess(
144 " l = lanes.linda()"
145 " l:send('k', {})" // send a table without a metatable
146 " key, out = l:receive('k')" // read it back
147 " assert(key == 'k' and type(out) == 'nil')" // should have become nil
148 );
149
150 S.requireSuccess(
151 " l = lanes.linda()"
152 " t = setmetatable({}, {__lanesconvert = 'decay'})" // override global converter with our own
153 " l:send('k', t)" // send the table
154 " key, out = l:receive('k')" // read it back
155 " assert(key == 'k' and type(out) == 'userdata', 'got ' .. key .. ' ' .. tostring(out))" // should have become a light userdata
156 );
157}
158
159// #################################################################################################
160// #################################################################################################
161
162TEST_CASE("misc.convert_max_attempts.is_respected")
163{
164 LuaState S{ LuaState::WithBaseLibs{ true }, LuaState::WithFixture{ false } };
165 S.requireSuccess("lanes = require 'lanes'; lanes.configure{convert_max_attempts = 3}");
166 S.requireSuccess("l = lanes.linda()");
167
168 S.requireSuccess(
169 " t = setmetatable({n=1}, {__lanesconvert = function(t, hint) t.n = t.n - 1 return t.n > 0 and t or 'done' end})" // table with a string-converter
170 " l:send('k', t)" // send the table
171 " key, out = l:receive('k')" // read it back
172 " assert(key == 'k' and out == 'done', 'got ' .. key .. ' ' .. tostring(out))" // should have stayed a table
173 );
174
175 S.requireSuccess(
176 " t = setmetatable({n=2}, {__lanesconvert = function(t, hint) t.n = t.n - 1 return t.n > 0 and t or 'done' end})" // table with a string-converter
177 " l:send('k', t)" // send the table
178 " key, out = l:receive('k')" // read it back
179 " assert(key == 'k' and out == 'done', 'got ' .. key .. ' ' .. tostring(out))" // should have stayed a table
180 );
181
182 S.requireSuccess(
183 " t = setmetatable({n=3}, {__lanesconvert = function(t, hint) t.n = t.n - 1 return t.n > 0 and t or 'done' end})" // table with a string-converter
184 " l:send('k', t)" // send the table
185 " key, out = l:receive('k')" // read it back
186 " assert(key == 'k' and out == 'done', 'got ' .. key .. ' ' .. tostring(out))" // should have stayed a table
187 );
188
189 S.requireFailure(
190 " t = setmetatable({n=4}, {__lanesconvert = function(t, hint) t.n = t.n - 1 return t.n > 0 and t or 'done' end})" // table with a string-converter
191 " l:send('k', t)" // send the table, it should raise an error because the converter retries too many times
192 );
193}
diff --git a/unit_tests/shared.cpp b/unit_tests/shared.cpp
index 9f3b08e..825cd48 100644
--- a/unit_tests/shared.cpp
+++ b/unit_tests/shared.cpp
@@ -46,8 +46,14 @@ namespace
46 return 1; 46 return 1;
47 }; 47 };
48 48
49 // a function that creates a full userdata, using first argument as its metatable
49 lua_CFunction sNewUserData = +[](lua_State* const L_) { 50 lua_CFunction sNewUserData = +[](lua_State* const L_) {
50 std::ignore = luaW_newuserdatauv<int>(L_, UserValueCount{ 0 }); 51 lua_settop(L_, 1); // L_: {}|nil
52 STACK_CHECK_START_ABS(L_, 1);
53 std::ignore = luaW_newuserdatauv<int>(L_, UserValueCount{ 0 }); // L_: {}|nil u
54 lua_insert(L_, 1); // L_: u {}|nil
55 lua_setmetatable(L_, 1); // L_: u
56 STACK_CHECK(L_, 1);
51 return 1; 57 return 1;
52 }; 58 };
53 59