diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 0000000..f173110 --- /dev/null +++ b/.nojekyll @@ -0,0 +1 @@ +This file makes sure that Github Pages doesn't process mdBook's output. diff --git a/404.html b/404.html new file mode 100644 index 0000000..9549822 --- /dev/null +++ b/404.html @@ -0,0 +1,200 @@ + + + + + + Page not found - huangjj27's Tech blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+
+

Document not found (404)

+

This URL is invalid, sorry. Please use the navigation bar or search to continue.

+ +
+ +
+ + + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + diff --git a/FontAwesome/css/font-awesome.css b/FontAwesome/css/font-awesome.css new file mode 100644 index 0000000..540440c --- /dev/null +++ b/FontAwesome/css/font-awesome.css @@ -0,0 +1,4 @@ +/*! + * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome + * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) + */@font-face{font-family:'FontAwesome';src:url('../fonts/fontawesome-webfont.eot?v=4.7.0');src:url('../fonts/fontawesome-webfont.eot?#iefix&v=4.7.0') format('embedded-opentype'),url('../fonts/fontawesome-webfont.woff2?v=4.7.0') format('woff2'),url('../fonts/fontawesome-webfont.woff?v=4.7.0') format('woff'),url('../fonts/fontawesome-webfont.ttf?v=4.7.0') format('truetype'),url('../fonts/fontawesome-webfont.svg?v=4.7.0#fontawesomeregular') format('svg');font-weight:normal;font-style:normal}.fa{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571429em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14285714em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14285714em;width:2.14285714em;top:.14285714em;text-align:center}.fa-li.fa-lg{left:-1.85714286em}.fa-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left{margin-right:.3em}.fa.fa-pull-right{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scale(-1, 1);-ms-transform:scale(-1, 1);transform:scale(-1, 1)}.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";-webkit-transform:scale(1, -1);-ms-transform:scale(1, -1);transform:scale(1, -1)}:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-remove:before,.fa-close:before,.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-gear:before,.fa-cog:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-rotate-right:before,.fa-repeat:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-photo:before,.fa-image:before,.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before,.fa-bar-chart:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-gears:before,.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook-f:before,.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-feed:before,.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-save:before,.fa-floppy-o:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-navicon:before,.fa-reorder:before,.fa-bars:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-unsorted:before,.fa-sort:before{content:"\f0dc"}.fa-sort-down:before,.fa-sort-desc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-asc:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-legal:before,.fa-gavel:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-flash:before,.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before,.fa-clipboard:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-unlink:before,.fa-chain-broken:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"}.fa-euro:before,.fa-eur:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-rupee:before,.fa-inr:before{content:"\f156"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"}.fa-won:before,.fa-krw:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before,.fa-gratipay:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-turkish-lira:before,.fa-try:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"}.fa-space-shuttle:before{content:"\f197"}.fa-slack:before{content:"\f198"}.fa-envelope-square:before{content:"\f199"}.fa-wordpress:before{content:"\f19a"}.fa-openid:before{content:"\f19b"}.fa-institution:before,.fa-bank:before,.fa-university:before{content:"\f19c"}.fa-mortar-board:before,.fa-graduation-cap:before{content:"\f19d"}.fa-yahoo:before{content:"\f19e"}.fa-google:before{content:"\f1a0"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-square:before{content:"\f1a2"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-stumbleupon:before{content:"\f1a4"}.fa-delicious:before{content:"\f1a5"}.fa-digg:before{content:"\f1a6"}.fa-pied-piper-pp:before{content:"\f1a7"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-drupal:before{content:"\f1a9"}.fa-joomla:before{content:"\f1aa"}.fa-language:before{content:"\f1ab"}.fa-fax:before{content:"\f1ac"}.fa-building:before{content:"\f1ad"}.fa-child:before{content:"\f1ae"}.fa-paw:before{content:"\f1b0"}.fa-spoon:before{content:"\f1b1"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-recycle:before{content:"\f1b8"}.fa-automobile:before,.fa-car:before{content:"\f1b9"}.fa-cab:before,.fa-taxi:before{content:"\f1ba"}.fa-tree:before{content:"\f1bb"}.fa-spotify:before{content:"\f1bc"}.fa-deviantart:before{content:"\f1bd"}.fa-soundcloud:before{content:"\f1be"}.fa-database:before{content:"\f1c0"}.fa-file-pdf-o:before{content:"\f1c1"}.fa-file-word-o:before{content:"\f1c2"}.fa-file-excel-o:before{content:"\f1c3"}.fa-file-powerpoint-o:before{content:"\f1c4"}.fa-file-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:"\f1c5"}.fa-file-zip-o:before,.fa-file-archive-o:before{content:"\f1c6"}.fa-file-sound-o:before,.fa-file-audio-o:before{content:"\f1c7"}.fa-file-movie-o:before,.fa-file-video-o:before{content:"\f1c8"}.fa-file-code-o:before{content:"\f1c9"}.fa-vine:before{content:"\f1ca"}.fa-codepen:before{content:"\f1cb"}.fa-jsfiddle:before{content:"\f1cc"}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-saver:before,.fa-support:before,.fa-life-ring:before{content:"\f1cd"}.fa-circle-o-notch:before{content:"\f1ce"}.fa-ra:before,.fa-resistance:before,.fa-rebel:before{content:"\f1d0"}.fa-ge:before,.fa-empire:before{content:"\f1d1"}.fa-git-square:before{content:"\f1d2"}.fa-git:before{content:"\f1d3"}.fa-y-combinator-square:before,.fa-yc-square:before,.fa-hacker-news:before{content:"\f1d4"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-qq:before{content:"\f1d6"}.fa-wechat:before,.fa-weixin:before{content:"\f1d7"}.fa-send:before,.fa-paper-plane:before{content:"\f1d8"}.fa-send-o:before,.fa-paper-plane-o:before{content:"\f1d9"}.fa-history:before{content:"\f1da"}.fa-circle-thin:before{content:"\f1db"}.fa-header:before{content:"\f1dc"}.fa-paragraph:before{content:"\f1dd"}.fa-sliders:before{content:"\f1de"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-bomb:before{content:"\f1e2"}.fa-soccer-ball-o:before,.fa-futbol-o:before{content:"\f1e3"}.fa-tty:before{content:"\f1e4"}.fa-binoculars:before{content:"\f1e5"}.fa-plug:before{content:"\f1e6"}.fa-slideshare:before{content:"\f1e7"}.fa-twitch:before{content:"\f1e8"}.fa-yelp:before{content:"\f1e9"}.fa-newspaper-o:before{content:"\f1ea"}.fa-wifi:before{content:"\f1eb"}.fa-calculator:before{content:"\f1ec"}.fa-paypal:before{content:"\f1ed"}.fa-google-wallet:before{content:"\f1ee"}.fa-cc-visa:before{content:"\f1f0"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-bell-slash:before{content:"\f1f6"}.fa-bell-slash-o:before{content:"\f1f7"}.fa-trash:before{content:"\f1f8"}.fa-copyright:before{content:"\f1f9"}.fa-at:before{content:"\f1fa"}.fa-eyedropper:before{content:"\f1fb"}.fa-paint-brush:before{content:"\f1fc"}.fa-birthday-cake:before{content:"\f1fd"}.fa-area-chart:before{content:"\f1fe"}.fa-pie-chart:before{content:"\f200"}.fa-line-chart:before{content:"\f201"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-bicycle:before{content:"\f206"}.fa-bus:before{content:"\f207"}.fa-ioxhost:before{content:"\f208"}.fa-angellist:before{content:"\f209"}.fa-cc:before{content:"\f20a"}.fa-shekel:before,.fa-sheqel:before,.fa-ils:before{content:"\f20b"}.fa-meanpath:before{content:"\f20c"}.fa-buysellads:before{content:"\f20d"}.fa-connectdevelop:before{content:"\f20e"}.fa-dashcube:before{content:"\f210"}.fa-forumbee:before{content:"\f211"}.fa-leanpub:before{content:"\f212"}.fa-sellsy:before{content:"\f213"}.fa-shirtsinbulk:before{content:"\f214"}.fa-simplybuilt:before{content:"\f215"}.fa-skyatlas:before{content:"\f216"}.fa-cart-plus:before{content:"\f217"}.fa-cart-arrow-down:before{content:"\f218"}.fa-diamond:before{content:"\f219"}.fa-ship:before{content:"\f21a"}.fa-user-secret:before{content:"\f21b"}.fa-motorcycle:before{content:"\f21c"}.fa-street-view:before{content:"\f21d"}.fa-heartbeat:before{content:"\f21e"}.fa-venus:before{content:"\f221"}.fa-mars:before{content:"\f222"}.fa-mercury:before{content:"\f223"}.fa-intersex:before,.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-venus-double:before{content:"\f226"}.fa-mars-double:before{content:"\f227"}.fa-venus-mars:before{content:"\f228"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-neuter:before{content:"\f22c"}.fa-genderless:before{content:"\f22d"}.fa-facebook-official:before{content:"\f230"}.fa-pinterest-p:before{content:"\f231"}.fa-whatsapp:before{content:"\f232"}.fa-server:before{content:"\f233"}.fa-user-plus:before{content:"\f234"}.fa-user-times:before{content:"\f235"}.fa-hotel:before,.fa-bed:before{content:"\f236"}.fa-viacoin:before{content:"\f237"}.fa-train:before{content:"\f238"}.fa-subway:before{content:"\f239"}.fa-medium:before{content:"\f23a"}.fa-yc:before,.fa-y-combinator:before{content:"\f23b"}.fa-optin-monster:before{content:"\f23c"}.fa-opencart:before{content:"\f23d"}.fa-expeditedssl:before{content:"\f23e"}.fa-battery-4:before,.fa-battery:before,.fa-battery-full:before{content:"\f240"}.fa-battery-3:before,.fa-battery-three-quarters:before{content:"\f241"}.fa-battery-2:before,.fa-battery-half:before{content:"\f242"}.fa-battery-1:before,.fa-battery-quarter:before{content:"\f243"}.fa-battery-0:before,.fa-battery-empty:before{content:"\f244"}.fa-mouse-pointer:before{content:"\f245"}.fa-i-cursor:before{content:"\f246"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-sticky-note:before{content:"\f249"}.fa-sticky-note-o:before{content:"\f24a"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-diners-club:before{content:"\f24c"}.fa-clone:before{content:"\f24d"}.fa-balance-scale:before{content:"\f24e"}.fa-hourglass-o:before{content:"\f250"}.fa-hourglass-1:before,.fa-hourglass-start:before{content:"\f251"}.fa-hourglass-2:before,.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-3:before,.fa-hourglass-end:before{content:"\f253"}.fa-hourglass:before{content:"\f254"}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:"\f255"}.fa-hand-stop-o:before,.fa-hand-paper-o:before{content:"\f256"}.fa-hand-scissors-o:before{content:"\f257"}.fa-hand-lizard-o:before{content:"\f258"}.fa-hand-spock-o:before{content:"\f259"}.fa-hand-pointer-o:before{content:"\f25a"}.fa-hand-peace-o:before{content:"\f25b"}.fa-trademark:before{content:"\f25c"}.fa-registered:before{content:"\f25d"}.fa-creative-commons:before{content:"\f25e"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-tripadvisor:before{content:"\f262"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-get-pocket:before{content:"\f265"}.fa-wikipedia-w:before{content:"\f266"}.fa-safari:before{content:"\f267"}.fa-chrome:before{content:"\f268"}.fa-firefox:before{content:"\f269"}.fa-opera:before{content:"\f26a"}.fa-internet-explorer:before{content:"\f26b"}.fa-tv:before,.fa-television:before{content:"\f26c"}.fa-contao:before{content:"\f26d"}.fa-500px:before{content:"\f26e"}.fa-amazon:before{content:"\f270"}.fa-calendar-plus-o:before{content:"\f271"}.fa-calendar-minus-o:before{content:"\f272"}.fa-calendar-times-o:before{content:"\f273"}.fa-calendar-check-o:before{content:"\f274"}.fa-industry:before{content:"\f275"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-map-o:before{content:"\f278"}.fa-map:before{content:"\f279"}.fa-commenting:before{content:"\f27a"}.fa-commenting-o:before{content:"\f27b"}.fa-houzz:before{content:"\f27c"}.fa-vimeo:before{content:"\f27d"}.fa-black-tie:before{content:"\f27e"}.fa-fonticons:before{content:"\f280"}.fa-reddit-alien:before{content:"\f281"}.fa-edge:before{content:"\f282"}.fa-credit-card-alt:before{content:"\f283"}.fa-codiepie:before{content:"\f284"}.fa-modx:before{content:"\f285"}.fa-fort-awesome:before{content:"\f286"}.fa-usb:before{content:"\f287"}.fa-product-hunt:before{content:"\f288"}.fa-mixcloud:before{content:"\f289"}.fa-scribd:before{content:"\f28a"}.fa-pause-circle:before{content:"\f28b"}.fa-pause-circle-o:before{content:"\f28c"}.fa-stop-circle:before{content:"\f28d"}.fa-stop-circle-o:before{content:"\f28e"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-hashtag:before{content:"\f292"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-percent:before{content:"\f295"}.fa-gitlab:before{content:"\f296"}.fa-wpbeginner:before{content:"\f297"}.fa-wpforms:before{content:"\f298"}.fa-envira:before{content:"\f299"}.fa-universal-access:before{content:"\f29a"}.fa-wheelchair-alt:before{content:"\f29b"}.fa-question-circle-o:before{content:"\f29c"}.fa-blind:before{content:"\f29d"}.fa-audio-description:before{content:"\f29e"}.fa-volume-control-phone:before{content:"\f2a0"}.fa-braille:before{content:"\f2a1"}.fa-assistive-listening-systems:before{content:"\f2a2"}.fa-asl-interpreting:before,.fa-american-sign-language-interpreting:before{content:"\f2a3"}.fa-deafness:before,.fa-hard-of-hearing:before,.fa-deaf:before{content:"\f2a4"}.fa-glide:before{content:"\f2a5"}.fa-glide-g:before{content:"\f2a6"}.fa-signing:before,.fa-sign-language:before{content:"\f2a7"}.fa-low-vision:before{content:"\f2a8"}.fa-viadeo:before{content:"\f2a9"}.fa-viadeo-square:before{content:"\f2aa"}.fa-snapchat:before{content:"\f2ab"}.fa-snapchat-ghost:before{content:"\f2ac"}.fa-snapchat-square:before{content:"\f2ad"}.fa-pied-piper:before{content:"\f2ae"}.fa-first-order:before{content:"\f2b0"}.fa-yoast:before{content:"\f2b1"}.fa-themeisle:before{content:"\f2b2"}.fa-google-plus-circle:before,.fa-google-plus-official:before{content:"\f2b3"}.fa-fa:before,.fa-font-awesome:before{content:"\f2b4"}.fa-handshake-o:before{content:"\f2b5"}.fa-envelope-open:before{content:"\f2b6"}.fa-envelope-open-o:before{content:"\f2b7"}.fa-linode:before{content:"\f2b8"}.fa-address-book:before{content:"\f2b9"}.fa-address-book-o:before{content:"\f2ba"}.fa-vcard:before,.fa-address-card:before{content:"\f2bb"}.fa-vcard-o:before,.fa-address-card-o:before{content:"\f2bc"}.fa-user-circle:before{content:"\f2bd"}.fa-user-circle-o:before{content:"\f2be"}.fa-user-o:before{content:"\f2c0"}.fa-id-badge:before{content:"\f2c1"}.fa-drivers-license:before,.fa-id-card:before{content:"\f2c2"}.fa-drivers-license-o:before,.fa-id-card-o:before{content:"\f2c3"}.fa-quora:before{content:"\f2c4"}.fa-free-code-camp:before{content:"\f2c5"}.fa-telegram:before{content:"\f2c6"}.fa-thermometer-4:before,.fa-thermometer:before,.fa-thermometer-full:before{content:"\f2c7"}.fa-thermometer-3:before,.fa-thermometer-three-quarters:before{content:"\f2c8"}.fa-thermometer-2:before,.fa-thermometer-half:before{content:"\f2c9"}.fa-thermometer-1:before,.fa-thermometer-quarter:before{content:"\f2ca"}.fa-thermometer-0:before,.fa-thermometer-empty:before{content:"\f2cb"}.fa-shower:before{content:"\f2cc"}.fa-bathtub:before,.fa-s15:before,.fa-bath:before{content:"\f2cd"}.fa-podcast:before{content:"\f2ce"}.fa-window-maximize:before{content:"\f2d0"}.fa-window-minimize:before{content:"\f2d1"}.fa-window-restore:before{content:"\f2d2"}.fa-times-rectangle:before,.fa-window-close:before{content:"\f2d3"}.fa-times-rectangle-o:before,.fa-window-close-o:before{content:"\f2d4"}.fa-bandcamp:before{content:"\f2d5"}.fa-grav:before{content:"\f2d6"}.fa-etsy:before{content:"\f2d7"}.fa-imdb:before{content:"\f2d8"}.fa-ravelry:before{content:"\f2d9"}.fa-eercast:before{content:"\f2da"}.fa-microchip:before{content:"\f2db"}.fa-snowflake-o:before{content:"\f2dc"}.fa-superpowers:before{content:"\f2dd"}.fa-wpexplorer:before{content:"\f2de"}.fa-meetup:before{content:"\f2e0"}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0, 0, 0, 0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto} diff --git a/FontAwesome/fonts/FontAwesome.ttf b/FontAwesome/fonts/FontAwesome.ttf new file mode 100644 index 0000000..35acda2 Binary files /dev/null and b/FontAwesome/fonts/FontAwesome.ttf differ diff --git a/FontAwesome/fonts/fontawesome-webfont.eot b/FontAwesome/fonts/fontawesome-webfont.eot new file mode 100644 index 0000000..e9f60ca Binary files /dev/null and b/FontAwesome/fonts/fontawesome-webfont.eot differ diff --git a/FontAwesome/fonts/fontawesome-webfont.svg b/FontAwesome/fonts/fontawesome-webfont.svg new file mode 100644 index 0000000..855c845 --- /dev/null +++ b/FontAwesome/fonts/fontawesome-webfont.svg @@ -0,0 +1,2671 @@ + + + + +Created by FontForge 20120731 at Mon Oct 24 17:37:40 2016 + By ,,, +Copyright Dave Gandy 2016. All rights reserved. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/FontAwesome/fonts/fontawesome-webfont.ttf b/FontAwesome/fonts/fontawesome-webfont.ttf new file mode 100644 index 0000000..35acda2 Binary files /dev/null and b/FontAwesome/fonts/fontawesome-webfont.ttf differ diff --git a/FontAwesome/fonts/fontawesome-webfont.woff b/FontAwesome/fonts/fontawesome-webfont.woff new file mode 100644 index 0000000..400014a Binary files /dev/null and b/FontAwesome/fonts/fontawesome-webfont.woff differ diff --git a/FontAwesome/fonts/fontawesome-webfont.woff2 b/FontAwesome/fonts/fontawesome-webfont.woff2 new file mode 100644 index 0000000..4d13fc6 Binary files /dev/null and b/FontAwesome/fonts/fontawesome-webfont.woff2 differ diff --git a/ayu-highlight.css b/ayu-highlight.css new file mode 100644 index 0000000..32c9432 --- /dev/null +++ b/ayu-highlight.css @@ -0,0 +1,78 @@ +/* +Based off of the Ayu theme +Original by Dempfi (https://github.com/dempfi/ayu) +*/ + +.hljs { + display: block; + overflow-x: auto; + background: #191f26; + color: #e6e1cf; +} + +.hljs-comment, +.hljs-quote { + color: #5c6773; + font-style: italic; +} + +.hljs-variable, +.hljs-template-variable, +.hljs-attribute, +.hljs-attr, +.hljs-regexp, +.hljs-link, +.hljs-selector-id, +.hljs-selector-class { + color: #ff7733; +} + +.hljs-number, +.hljs-meta, +.hljs-builtin-name, +.hljs-literal, +.hljs-type, +.hljs-params { + color: #ffee99; +} + +.hljs-string, +.hljs-bullet { + color: #b8cc52; +} + +.hljs-title, +.hljs-built_in, +.hljs-section { + color: #ffb454; +} + +.hljs-keyword, +.hljs-selector-tag, +.hljs-symbol { + color: #ff7733; +} + +.hljs-name { + color: #36a3d9; +} + +.hljs-tag { + color: #00568d; +} + +.hljs-emphasis { + font-style: italic; +} + +.hljs-strong { + font-weight: bold; +} + +.hljs-addition { + color: #91b362; +} + +.hljs-deletion { + color: #d96c75; +} diff --git a/book.js b/book.js new file mode 100644 index 0000000..d40440c --- /dev/null +++ b/book.js @@ -0,0 +1,679 @@ +"use strict"; + +// Fix back button cache problem +window.onunload = function () { }; + +// Global variable, shared between modules +function playground_text(playground) { + let code_block = playground.querySelector("code"); + + if (window.ace && code_block.classList.contains("editable")) { + let editor = window.ace.edit(code_block); + return editor.getValue(); + } else { + return code_block.textContent; + } +} + +(function codeSnippets() { + function fetch_with_timeout(url, options, timeout = 6000) { + return Promise.race([ + fetch(url, options), + new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), timeout)) + ]); + } + + var playgrounds = Array.from(document.querySelectorAll(".playground")); + if (playgrounds.length > 0) { + fetch_with_timeout("https://play.rust-lang.org/meta/crates", { + headers: { + 'Content-Type': "application/json", + }, + method: 'POST', + mode: 'cors', + }) + .then(response => response.json()) + .then(response => { + // get list of crates available in the rust playground + let playground_crates = response.crates.map(item => item["id"]); + playgrounds.forEach(block => handle_crate_list_update(block, playground_crates)); + }); + } + + function handle_crate_list_update(playground_block, playground_crates) { + // update the play buttons after receiving the response + update_play_button(playground_block, playground_crates); + + // and install on change listener to dynamically update ACE editors + if (window.ace) { + let code_block = playground_block.querySelector("code"); + if (code_block.classList.contains("editable")) { + let editor = window.ace.edit(code_block); + editor.addEventListener("change", function (e) { + update_play_button(playground_block, playground_crates); + }); + // add Ctrl-Enter command to execute rust code + editor.commands.addCommand({ + name: "run", + bindKey: { + win: "Ctrl-Enter", + mac: "Ctrl-Enter" + }, + exec: _editor => run_rust_code(playground_block) + }); + } + } + } + + // updates the visibility of play button based on `no_run` class and + // used crates vs ones available on http://play.rust-lang.org + function update_play_button(pre_block, playground_crates) { + var play_button = pre_block.querySelector(".play-button"); + + // skip if code is `no_run` + if (pre_block.querySelector('code').classList.contains("no_run")) { + play_button.classList.add("hidden"); + return; + } + + // get list of `extern crate`'s from snippet + var txt = playground_text(pre_block); + var re = /extern\s+crate\s+([a-zA-Z_0-9]+)\s*;/g; + var snippet_crates = []; + var item; + while (item = re.exec(txt)) { + snippet_crates.push(item[1]); + } + + // check if all used crates are available on play.rust-lang.org + var all_available = snippet_crates.every(function (elem) { + return playground_crates.indexOf(elem) > -1; + }); + + if (all_available) { + play_button.classList.remove("hidden"); + } else { + play_button.classList.add("hidden"); + } + } + + function run_rust_code(code_block) { + var result_block = code_block.querySelector(".result"); + if (!result_block) { + result_block = document.createElement('code'); + result_block.className = 'result hljs language-bash'; + + code_block.append(result_block); + } + + let text = playground_text(code_block); + let classes = code_block.querySelector('code').classList; + let edition = "2015"; + if(classes.contains("edition2018")) { + edition = "2018"; + } else if(classes.contains("edition2021")) { + edition = "2021"; + } + var params = { + version: "stable", + optimize: "0", + code: text, + edition: edition + }; + + if (text.indexOf("#![feature") !== -1) { + params.version = "nightly"; + } + + result_block.innerText = "Running..."; + + fetch_with_timeout("https://play.rust-lang.org/evaluate.json", { + headers: { + 'Content-Type': "application/json", + }, + method: 'POST', + mode: 'cors', + body: JSON.stringify(params) + }) + .then(response => response.json()) + .then(response => { + if (response.result.trim() === '') { + result_block.innerText = "No output"; + result_block.classList.add("result-no-output"); + } else { + result_block.innerText = response.result; + result_block.classList.remove("result-no-output"); + } + }) + .catch(error => result_block.innerText = "Playground Communication: " + error.message); + } + + // Syntax highlighting Configuration + hljs.configure({ + tabReplace: ' ', // 4 spaces + languages: [], // Languages used for auto-detection + }); + + let code_nodes = Array + .from(document.querySelectorAll('code')) + // Don't highlight `inline code` blocks in headers. + .filter(function (node) {return !node.parentElement.classList.contains("header"); }); + + if (window.ace) { + // language-rust class needs to be removed for editable + // blocks or highlightjs will capture events + code_nodes + .filter(function (node) {return node.classList.contains("editable"); }) + .forEach(function (block) { block.classList.remove('language-rust'); }); + + Array + code_nodes + .filter(function (node) {return !node.classList.contains("editable"); }) + .forEach(function (block) { hljs.highlightBlock(block); }); + } else { + code_nodes.forEach(function (block) { hljs.highlightBlock(block); }); + } + + // Adding the hljs class gives code blocks the color css + // even if highlighting doesn't apply + code_nodes.forEach(function (block) { block.classList.add('hljs'); }); + + Array.from(document.querySelectorAll("code.language-rust")).forEach(function (block) { + + var lines = Array.from(block.querySelectorAll('.boring')); + // If no lines were hidden, return + if (!lines.length) { return; } + block.classList.add("hide-boring"); + + var buttons = document.createElement('div'); + buttons.className = 'buttons'; + buttons.innerHTML = ""; + + // add expand button + var pre_block = block.parentNode; + pre_block.insertBefore(buttons, pre_block.firstChild); + + pre_block.querySelector('.buttons').addEventListener('click', function (e) { + if (e.target.classList.contains('fa-eye')) { + e.target.classList.remove('fa-eye'); + e.target.classList.add('fa-eye-slash'); + e.target.title = 'Hide lines'; + e.target.setAttribute('aria-label', e.target.title); + + block.classList.remove('hide-boring'); + } else if (e.target.classList.contains('fa-eye-slash')) { + e.target.classList.remove('fa-eye-slash'); + e.target.classList.add('fa-eye'); + e.target.title = 'Show hidden lines'; + e.target.setAttribute('aria-label', e.target.title); + + block.classList.add('hide-boring'); + } + }); + }); + + if (window.playground_copyable) { + Array.from(document.querySelectorAll('pre code')).forEach(function (block) { + var pre_block = block.parentNode; + if (!pre_block.classList.contains('playground')) { + var buttons = pre_block.querySelector(".buttons"); + if (!buttons) { + buttons = document.createElement('div'); + buttons.className = 'buttons'; + pre_block.insertBefore(buttons, pre_block.firstChild); + } + + var clipButton = document.createElement('button'); + clipButton.className = 'fa fa-copy clip-button'; + clipButton.title = 'Copy to clipboard'; + clipButton.setAttribute('aria-label', clipButton.title); + clipButton.innerHTML = ''; + + buttons.insertBefore(clipButton, buttons.firstChild); + } + }); + } + + // Process playground code blocks + Array.from(document.querySelectorAll(".playground")).forEach(function (pre_block) { + // Add play button + var buttons = pre_block.querySelector(".buttons"); + if (!buttons) { + buttons = document.createElement('div'); + buttons.className = 'buttons'; + pre_block.insertBefore(buttons, pre_block.firstChild); + } + + var runCodeButton = document.createElement('button'); + runCodeButton.className = 'fa fa-play play-button'; + runCodeButton.hidden = true; + runCodeButton.title = 'Run this code'; + runCodeButton.setAttribute('aria-label', runCodeButton.title); + + buttons.insertBefore(runCodeButton, buttons.firstChild); + runCodeButton.addEventListener('click', function (e) { + run_rust_code(pre_block); + }); + + if (window.playground_copyable) { + var copyCodeClipboardButton = document.createElement('button'); + copyCodeClipboardButton.className = 'fa fa-copy clip-button'; + copyCodeClipboardButton.innerHTML = ''; + copyCodeClipboardButton.title = 'Copy to clipboard'; + copyCodeClipboardButton.setAttribute('aria-label', copyCodeClipboardButton.title); + + buttons.insertBefore(copyCodeClipboardButton, buttons.firstChild); + } + + let code_block = pre_block.querySelector("code"); + if (window.ace && code_block.classList.contains("editable")) { + var undoChangesButton = document.createElement('button'); + undoChangesButton.className = 'fa fa-history reset-button'; + undoChangesButton.title = 'Undo changes'; + undoChangesButton.setAttribute('aria-label', undoChangesButton.title); + + buttons.insertBefore(undoChangesButton, buttons.firstChild); + + undoChangesButton.addEventListener('click', function () { + let editor = window.ace.edit(code_block); + editor.setValue(editor.originalCode); + editor.clearSelection(); + }); + } + }); +})(); + +(function themes() { + var html = document.querySelector('html'); + var themeToggleButton = document.getElementById('theme-toggle'); + var themePopup = document.getElementById('theme-list'); + var themeColorMetaTag = document.querySelector('meta[name="theme-color"]'); + var stylesheets = { + ayuHighlight: document.querySelector("[href$='ayu-highlight.css']"), + tomorrowNight: document.querySelector("[href$='tomorrow-night.css']"), + highlight: document.querySelector("[href$='highlight.css']"), + }; + + function showThemes() { + themePopup.style.display = 'block'; + themeToggleButton.setAttribute('aria-expanded', true); + themePopup.querySelector("button#" + get_theme()).focus(); + } + + function hideThemes() { + themePopup.style.display = 'none'; + themeToggleButton.setAttribute('aria-expanded', false); + themeToggleButton.focus(); + } + + function get_theme() { + var theme; + try { theme = localStorage.getItem('mdbook-theme'); } catch (e) { } + if (theme === null || theme === undefined) { + return default_theme; + } else { + return theme; + } + } + + function set_theme(theme, store = true) { + let ace_theme; + + if (theme == 'coal' || theme == 'navy') { + stylesheets.ayuHighlight.disabled = true; + stylesheets.tomorrowNight.disabled = false; + stylesheets.highlight.disabled = true; + + ace_theme = "ace/theme/tomorrow_night"; + } else if (theme == 'ayu') { + stylesheets.ayuHighlight.disabled = false; + stylesheets.tomorrowNight.disabled = true; + stylesheets.highlight.disabled = true; + ace_theme = "ace/theme/tomorrow_night"; + } else { + stylesheets.ayuHighlight.disabled = true; + stylesheets.tomorrowNight.disabled = true; + stylesheets.highlight.disabled = false; + ace_theme = "ace/theme/dawn"; + } + + setTimeout(function () { + themeColorMetaTag.content = getComputedStyle(document.body).backgroundColor; + }, 1); + + if (window.ace && window.editors) { + window.editors.forEach(function (editor) { + editor.setTheme(ace_theme); + }); + } + + var previousTheme = get_theme(); + + if (store) { + try { localStorage.setItem('mdbook-theme', theme); } catch (e) { } + } + + html.classList.remove(previousTheme); + html.classList.add(theme); + } + + // Set theme + var theme = get_theme(); + + set_theme(theme, false); + + themeToggleButton.addEventListener('click', function () { + if (themePopup.style.display === 'block') { + hideThemes(); + } else { + showThemes(); + } + }); + + themePopup.addEventListener('click', function (e) { + var theme; + if (e.target.className === "theme") { + theme = e.target.id; + } else if (e.target.parentElement.className === "theme") { + theme = e.target.parentElement.id; + } else { + return; + } + set_theme(theme); + }); + + themePopup.addEventListener('focusout', function(e) { + // e.relatedTarget is null in Safari and Firefox on macOS (see workaround below) + if (!!e.relatedTarget && !themeToggleButton.contains(e.relatedTarget) && !themePopup.contains(e.relatedTarget)) { + hideThemes(); + } + }); + + // Should not be needed, but it works around an issue on macOS & iOS: https://github.com/rust-lang/mdBook/issues/628 + document.addEventListener('click', function(e) { + if (themePopup.style.display === 'block' && !themeToggleButton.contains(e.target) && !themePopup.contains(e.target)) { + hideThemes(); + } + }); + + document.addEventListener('keydown', function (e) { + if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) { return; } + if (!themePopup.contains(e.target)) { return; } + + switch (e.key) { + case 'Escape': + e.preventDefault(); + hideThemes(); + break; + case 'ArrowUp': + e.preventDefault(); + var li = document.activeElement.parentElement; + if (li && li.previousElementSibling) { + li.previousElementSibling.querySelector('button').focus(); + } + break; + case 'ArrowDown': + e.preventDefault(); + var li = document.activeElement.parentElement; + if (li && li.nextElementSibling) { + li.nextElementSibling.querySelector('button').focus(); + } + break; + case 'Home': + e.preventDefault(); + themePopup.querySelector('li:first-child button').focus(); + break; + case 'End': + e.preventDefault(); + themePopup.querySelector('li:last-child button').focus(); + break; + } + }); +})(); + +(function sidebar() { + var html = document.querySelector("html"); + var sidebar = document.getElementById("sidebar"); + var sidebarLinks = document.querySelectorAll('#sidebar a'); + var sidebarToggleButton = document.getElementById("sidebar-toggle"); + var sidebarResizeHandle = document.getElementById("sidebar-resize-handle"); + var firstContact = null; + + function showSidebar() { + html.classList.remove('sidebar-hidden') + html.classList.add('sidebar-visible'); + Array.from(sidebarLinks).forEach(function (link) { + link.setAttribute('tabIndex', 0); + }); + sidebarToggleButton.setAttribute('aria-expanded', true); + sidebar.setAttribute('aria-hidden', false); + try { localStorage.setItem('mdbook-sidebar', 'visible'); } catch (e) { } + } + + + var sidebarAnchorToggles = document.querySelectorAll('#sidebar a.toggle'); + + function toggleSection(ev) { + ev.currentTarget.parentElement.classList.toggle('expanded'); + } + + Array.from(sidebarAnchorToggles).forEach(function (el) { + el.addEventListener('click', toggleSection); + }); + + function hideSidebar() { + html.classList.remove('sidebar-visible') + html.classList.add('sidebar-hidden'); + Array.from(sidebarLinks).forEach(function (link) { + link.setAttribute('tabIndex', -1); + }); + sidebarToggleButton.setAttribute('aria-expanded', false); + sidebar.setAttribute('aria-hidden', true); + try { localStorage.setItem('mdbook-sidebar', 'hidden'); } catch (e) { } + } + + // Toggle sidebar + sidebarToggleButton.addEventListener('click', function sidebarToggle() { + if (html.classList.contains("sidebar-hidden")) { + var current_width = parseInt( + document.documentElement.style.getPropertyValue('--sidebar-width'), 10); + if (current_width < 150) { + document.documentElement.style.setProperty('--sidebar-width', '150px'); + } + showSidebar(); + } else if (html.classList.contains("sidebar-visible")) { + hideSidebar(); + } else { + if (getComputedStyle(sidebar)['transform'] === 'none') { + hideSidebar(); + } else { + showSidebar(); + } + } + }); + + sidebarResizeHandle.addEventListener('mousedown', initResize, false); + + function initResize(e) { + window.addEventListener('mousemove', resize, false); + window.addEventListener('mouseup', stopResize, false); + html.classList.add('sidebar-resizing'); + } + function resize(e) { + var pos = (e.clientX - sidebar.offsetLeft); + if (pos < 20) { + hideSidebar(); + } else { + if (html.classList.contains("sidebar-hidden")) { + showSidebar(); + } + pos = Math.min(pos, window.innerWidth - 100); + document.documentElement.style.setProperty('--sidebar-width', pos + 'px'); + } + } + //on mouseup remove windows functions mousemove & mouseup + function stopResize(e) { + html.classList.remove('sidebar-resizing'); + window.removeEventListener('mousemove', resize, false); + window.removeEventListener('mouseup', stopResize, false); + } + + document.addEventListener('touchstart', function (e) { + firstContact = { + x: e.touches[0].clientX, + time: Date.now() + }; + }, { passive: true }); + + document.addEventListener('touchmove', function (e) { + if (!firstContact) + return; + + var curX = e.touches[0].clientX; + var xDiff = curX - firstContact.x, + tDiff = Date.now() - firstContact.time; + + if (tDiff < 250 && Math.abs(xDiff) >= 150) { + if (xDiff >= 0 && firstContact.x < Math.min(document.body.clientWidth * 0.25, 300)) + showSidebar(); + else if (xDiff < 0 && curX < 300) + hideSidebar(); + + firstContact = null; + } + }, { passive: true }); + + // Scroll sidebar to current active section + var activeSection = document.getElementById("sidebar").querySelector(".active"); + if (activeSection) { + // https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView + activeSection.scrollIntoView({ block: 'center' }); + } +})(); + +(function chapterNavigation() { + document.addEventListener('keydown', function (e) { + if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) { return; } + if (window.search && window.search.hasFocus()) { return; } + + switch (e.key) { + case 'ArrowRight': + e.preventDefault(); + var nextButton = document.querySelector('.nav-chapters.next'); + if (nextButton) { + window.location.href = nextButton.href; + } + break; + case 'ArrowLeft': + e.preventDefault(); + var previousButton = document.querySelector('.nav-chapters.previous'); + if (previousButton) { + window.location.href = previousButton.href; + } + break; + } + }); +})(); + +(function clipboard() { + var clipButtons = document.querySelectorAll('.clip-button'); + + function hideTooltip(elem) { + elem.firstChild.innerText = ""; + elem.className = 'fa fa-copy clip-button'; + } + + function showTooltip(elem, msg) { + elem.firstChild.innerText = msg; + elem.className = 'fa fa-copy tooltipped'; + } + + var clipboardSnippets = new ClipboardJS('.clip-button', { + text: function (trigger) { + hideTooltip(trigger); + let playground = trigger.closest("pre"); + return playground_text(playground); + } + }); + + Array.from(clipButtons).forEach(function (clipButton) { + clipButton.addEventListener('mouseout', function (e) { + hideTooltip(e.currentTarget); + }); + }); + + clipboardSnippets.on('success', function (e) { + e.clearSelection(); + showTooltip(e.trigger, "Copied!"); + }); + + clipboardSnippets.on('error', function (e) { + showTooltip(e.trigger, "Clipboard error!"); + }); +})(); + +(function scrollToTop () { + var menuTitle = document.querySelector('.menu-title'); + + menuTitle.addEventListener('click', function () { + document.scrollingElement.scrollTo({ top: 0, behavior: 'smooth' }); + }); +})(); + +(function controllMenu() { + var menu = document.getElementById('menu-bar'); + + (function controllPosition() { + var scrollTop = document.scrollingElement.scrollTop; + var prevScrollTop = scrollTop; + var minMenuY = -menu.clientHeight - 50; + // When the script loads, the page can be at any scroll (e.g. if you reforesh it). + menu.style.top = scrollTop + 'px'; + // Same as parseInt(menu.style.top.slice(0, -2), but faster + var topCache = menu.style.top.slice(0, -2); + menu.classList.remove('sticky'); + var stickyCache = false; // Same as menu.classList.contains('sticky'), but faster + document.addEventListener('scroll', function () { + scrollTop = Math.max(document.scrollingElement.scrollTop, 0); + // `null` means that it doesn't need to be updated + var nextSticky = null; + var nextTop = null; + var scrollDown = scrollTop > prevScrollTop; + var menuPosAbsoluteY = topCache - scrollTop; + if (scrollDown) { + nextSticky = false; + if (menuPosAbsoluteY > 0) { + nextTop = prevScrollTop; + } + } else { + if (menuPosAbsoluteY > 0) { + nextSticky = true; + } else if (menuPosAbsoluteY < minMenuY) { + nextTop = prevScrollTop + minMenuY; + } + } + if (nextSticky === true && stickyCache === false) { + menu.classList.add('sticky'); + stickyCache = true; + } else if (nextSticky === false && stickyCache === true) { + menu.classList.remove('sticky'); + stickyCache = false; + } + if (nextTop !== null) { + menu.style.top = nextTop + 'px'; + topCache = nextTop; + } + prevScrollTop = scrollTop; + }, { passive: true }); + })(); + (function controllBorder() { + menu.classList.remove('bordered'); + document.addEventListener('scroll', function () { + if (menu.offsetTop === 0) { + menu.classList.remove('bordered'); + } else { + menu.classList.add('bordered'); + } + }, { passive: true }); + })(); +})(); diff --git a/clipboard.min.js b/clipboard.min.js new file mode 100644 index 0000000..02c549e --- /dev/null +++ b/clipboard.min.js @@ -0,0 +1,7 @@ +/*! + * clipboard.js v2.0.4 + * https://zenorocha.github.io/clipboard.js + * + * Licensed MIT © Zeno Rocha + */ +!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.ClipboardJS=e():t.ClipboardJS=e()}(this,function(){return function(n){var o={};function r(t){if(o[t])return o[t].exports;var e=o[t]={i:t,l:!1,exports:{}};return n[t].call(e.exports,e,e.exports,r),e.l=!0,e.exports}return r.m=n,r.c=o,r.d=function(t,e,n){r.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:n})},r.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},r.t=function(e,t){if(1&t&&(e=r(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(r.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)r.d(n,o,function(t){return e[t]}.bind(null,o));return n},r.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return r.d(e,"a",e),e},r.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},r.p="",r(r.s=0)}([function(t,e,n){"use strict";var r="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},i=function(){function o(t,e){for(var n=0;n .hljs { + color: var(--links); +} + +/* Menu Bar */ + +#menu-bar, +#menu-bar-hover-placeholder { + z-index: 101; + margin: auto calc(0px - var(--page-padding)); +} +#menu-bar { + position: relative; + display: flex; + flex-wrap: wrap; + background-color: var(--bg); + border-bottom-color: var(--bg); + border-bottom-width: 1px; + border-bottom-style: solid; +} +#menu-bar.sticky, +.js #menu-bar-hover-placeholder:hover + #menu-bar, +.js #menu-bar:hover, +.js.sidebar-visible #menu-bar { + position: -webkit-sticky; + position: sticky; + top: 0 !important; +} +#menu-bar-hover-placeholder { + position: sticky; + position: -webkit-sticky; + top: 0; + height: var(--menu-bar-height); +} +#menu-bar.bordered { + border-bottom-color: var(--table-border-color); +} +#menu-bar i, #menu-bar .icon-button { + position: relative; + padding: 0 8px; + z-index: 10; + line-height: var(--menu-bar-height); + cursor: pointer; + transition: color 0.5s; +} +@media only screen and (max-width: 420px) { + #menu-bar i, #menu-bar .icon-button { + padding: 0 5px; + } +} + +.icon-button { + border: none; + background: none; + padding: 0; + color: inherit; +} +.icon-button i { + margin: 0; +} + +.right-buttons { + margin: 0 15px; +} +.right-buttons a { + text-decoration: none; +} + +.left-buttons { + display: flex; + margin: 0 5px; +} +.no-js .left-buttons { + display: none; +} + +.menu-title { + display: inline-block; + font-weight: 200; + font-size: 2.4rem; + line-height: var(--menu-bar-height); + text-align: center; + margin: 0; + flex: 1; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.js .menu-title { + cursor: pointer; +} + +.menu-bar, +.menu-bar:visited, +.nav-chapters, +.nav-chapters:visited, +.mobile-nav-chapters, +.mobile-nav-chapters:visited, +.menu-bar .icon-button, +.menu-bar a i { + color: var(--icons); +} + +.menu-bar i:hover, +.menu-bar .icon-button:hover, +.nav-chapters:hover, +.mobile-nav-chapters i:hover { + color: var(--icons-hover); +} + +/* Nav Icons */ + +.nav-chapters { + font-size: 2.5em; + text-align: center; + text-decoration: none; + + position: fixed; + top: 0; + bottom: 0; + margin: 0; + max-width: 150px; + min-width: 90px; + + display: flex; + justify-content: center; + align-content: center; + flex-direction: column; + + transition: color 0.5s, background-color 0.5s; +} + +.nav-chapters:hover { + text-decoration: none; + background-color: var(--theme-hover); + transition: background-color 0.15s, color 0.15s; +} + +.nav-wrapper { + margin-top: 50px; + display: none; +} + +.mobile-nav-chapters { + font-size: 2.5em; + text-align: center; + text-decoration: none; + width: 90px; + border-radius: 5px; + background-color: var(--sidebar-bg); +} + +.previous { + float: left; +} + +.next { + float: right; + right: var(--page-padding); +} + +@media only screen and (max-width: 1080px) { + .nav-wide-wrapper { display: none; } + .nav-wrapper { display: block; } +} + +@media only screen and (max-width: 1380px) { + .sidebar-visible .nav-wide-wrapper { display: none; } + .sidebar-visible .nav-wrapper { display: block; } +} + +/* Inline code */ + +:not(pre) > .hljs { + display: inline; + padding: 0.1em 0.3em; + border-radius: 3px; +} + +:not(pre):not(a) > .hljs { + color: var(--inline-code-color); + overflow-x: initial; +} + +a:hover > .hljs { + text-decoration: underline; +} + +pre { + position: relative; +} +pre > .buttons { + position: absolute; + z-index: 100; + right: 5px; + top: 5px; + + color: var(--sidebar-fg); + cursor: pointer; +} +pre > .buttons :hover { + color: var(--sidebar-active); +} +pre > .buttons i { + margin-left: 8px; +} +pre > .buttons button { + color: inherit; + background: transparent; + border: none; + cursor: inherit; +} +pre > .result { + margin-top: 10px; +} + +/* Search */ + +#searchresults a { + text-decoration: none; +} + +mark { + border-radius: 2px; + padding: 0 3px 1px 3px; + margin: 0 -3px -1px -3px; + background-color: var(--search-mark-bg); + transition: background-color 300ms linear; + cursor: pointer; +} + +mark.fade-out { + background-color: rgba(0,0,0,0) !important; + cursor: auto; +} + +.searchbar-outer { + margin-left: auto; + margin-right: auto; + max-width: var(--content-max-width); +} + +#searchbar { + width: 100%; + margin: 5px auto 0px auto; + padding: 10px 16px; + transition: box-shadow 300ms ease-in-out; + border: 1px solid var(--searchbar-border-color); + border-radius: 3px; + background-color: var(--searchbar-bg); + color: var(--searchbar-fg); +} +#searchbar:focus, +#searchbar.active { + box-shadow: 0 0 3px var(--searchbar-shadow-color); +} + +.searchresults-header { + font-weight: bold; + font-size: 1em; + padding: 18px 0 0 5px; + color: var(--searchresults-header-fg); +} + +.searchresults-outer { + margin-left: auto; + margin-right: auto; + max-width: var(--content-max-width); + border-bottom: 1px dashed var(--searchresults-border-color); +} + +ul#searchresults { + list-style: none; + padding-left: 20px; +} +ul#searchresults li { + margin: 10px 0px; + padding: 2px; + border-radius: 2px; +} +ul#searchresults li.focus { + background-color: var(--searchresults-li-bg); +} +ul#searchresults span.teaser { + display: block; + clear: both; + margin: 5px 0 0 20px; + font-size: 0.8em; +} +ul#searchresults span.teaser em { + font-weight: bold; + font-style: normal; +} + +/* Sidebar */ + +.sidebar { + position: fixed; + left: 0; + top: 0; + bottom: 0; + width: var(--sidebar-width); + font-size: 0.875em; + box-sizing: border-box; + -webkit-overflow-scrolling: touch; + overscroll-behavior-y: contain; + background-color: var(--sidebar-bg); + color: var(--sidebar-fg); +} +.sidebar-resizing { + -moz-user-select: none; + -webkit-user-select: none; + -ms-user-select: none; + user-select: none; +} +.js:not(.sidebar-resizing) .sidebar { + transition: transform 0.3s; /* Animation: slide away */ +} +.sidebar code { + line-height: 2em; +} +.sidebar .sidebar-scrollbox { + overflow-y: auto; + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + padding: 10px 10px; +} +.sidebar .sidebar-resize-handle { + position: absolute; + cursor: col-resize; + width: 0; + right: 0; + top: 0; + bottom: 0; +} +.js .sidebar .sidebar-resize-handle { + cursor: col-resize; + width: 5px; +} +.sidebar-hidden .sidebar { + transform: translateX(calc(0px - var(--sidebar-width))); +} +.sidebar::-webkit-scrollbar { + background: var(--sidebar-bg); +} +.sidebar::-webkit-scrollbar-thumb { + background: var(--scrollbar); +} + +.sidebar-visible .page-wrapper { + transform: translateX(var(--sidebar-width)); +} +@media only screen and (min-width: 620px) { + .sidebar-visible .page-wrapper { + transform: none; + margin-left: var(--sidebar-width); + } +} + +.chapter { + list-style: none outside none; + padding-left: 0; + line-height: 2.2em; +} + +.chapter ol { + width: 100%; +} + +.chapter li { + display: flex; + color: var(--sidebar-non-existant); +} +.chapter li a { + display: block; + padding: 0; + text-decoration: none; + color: var(--sidebar-fg); +} + +.chapter li a:hover { + color: var(--sidebar-active); +} + +.chapter li a.active { + color: var(--sidebar-active); +} + +.chapter li > a.toggle { + cursor: pointer; + display: block; + margin-left: auto; + padding: 0 10px; + user-select: none; + opacity: 0.68; +} + +.chapter li > a.toggle div { + transition: transform 0.5s; +} + +/* collapse the section */ +.chapter li:not(.expanded) + li > ol { + display: none; +} + +.chapter li.chapter-item { + line-height: 1.5em; + margin-top: 0.6em; +} + +.chapter li.expanded > a.toggle div { + transform: rotate(90deg); +} + +.spacer { + width: 100%; + height: 3px; + margin: 5px 0px; +} +.chapter .spacer { + background-color: var(--sidebar-spacer); +} + +@media (-moz-touch-enabled: 1), (pointer: coarse) { + .chapter li a { padding: 5px 0; } + .spacer { margin: 10px 0; } +} + +.section { + list-style: none outside none; + padding-left: 20px; + line-height: 1.9em; +} + +/* Theme Menu Popup */ + +.theme-popup { + position: absolute; + left: 10px; + top: var(--menu-bar-height); + z-index: 1000; + border-radius: 4px; + font-size: 0.7em; + color: var(--fg); + background: var(--theme-popup-bg); + border: 1px solid var(--theme-popup-border); + margin: 0; + padding: 0; + list-style: none; + display: none; +} +.theme-popup .default { + color: var(--icons); +} +.theme-popup .theme { + width: 100%; + border: 0; + margin: 0; + padding: 2px 10px; + line-height: 25px; + white-space: nowrap; + text-align: left; + cursor: pointer; + color: inherit; + background: inherit; + font-size: inherit; +} +.theme-popup .theme:hover { + background-color: var(--theme-hover); +} +.theme-popup .theme:hover:first-child, +.theme-popup .theme:hover:last-child { + border-top-left-radius: inherit; + border-top-right-radius: inherit; +} diff --git a/css/general.css b/css/general.css new file mode 100644 index 0000000..56ddd7e --- /dev/null +++ b/css/general.css @@ -0,0 +1,186 @@ +/* Base styles and content styles */ + +@import 'variables.css'; + +:root { + /* Browser default font-size is 16px, this way 1 rem = 10px */ + font-size: 62.5%; +} + +html { + font-family: "Open Sans", sans-serif; + color: var(--fg); + background-color: var(--bg); + text-size-adjust: none; + -webkit-text-size-adjust: none; +} + +body { + margin: 0; + font-size: 1.6rem; + overflow-x: hidden; +} + +code { + font-family: "Source Code Pro", Consolas, "Ubuntu Mono", Menlo, "DejaVu Sans Mono", monospace, monospace !important; + font-size: 0.875em; /* please adjust the ace font size accordingly in editor.js */ +} + +/* Don't change font size in headers. */ +h1 code, h2 code, h3 code, h4 code, h5 code, h6 code { + font-size: unset; +} + +.left { float: left; } +.right { float: right; } +.boring { opacity: 0.6; } +.hide-boring .boring { display: none; } +.hidden { display: none !important; } + +h2, h3 { margin-top: 2.5em; } +h4, h5 { margin-top: 2em; } + +.header + .header h3, +.header + .header h4, +.header + .header h5 { + margin-top: 1em; +} + +h1:target::before, +h2:target::before, +h3:target::before, +h4:target::before, +h5:target::before, +h6:target::before { + display: inline-block; + content: "»"; + margin-left: -30px; + width: 30px; +} + +/* This is broken on Safari as of version 14, but is fixed + in Safari Technology Preview 117 which I think will be Safari 14.2. + https://bugs.webkit.org/show_bug.cgi?id=218076 +*/ +:target { + scroll-margin-top: calc(var(--menu-bar-height) + 0.5em); +} + +.page { + outline: 0; + padding: 0 var(--page-padding); + margin-top: calc(0px - var(--menu-bar-height)); /* Compensate for the #menu-bar-hover-placeholder */ +} +.page-wrapper { + box-sizing: border-box; +} +.js:not(.sidebar-resizing) .page-wrapper { + transition: margin-left 0.3s ease, transform 0.3s ease; /* Animation: slide away */ +} + +.content { + overflow-y: auto; + padding: 0 15px; + padding-bottom: 50px; +} +.content main { + margin-left: auto; + margin-right: auto; + max-width: var(--content-max-width); +} +.content p { line-height: 1.45em; } +.content ol { line-height: 1.45em; } +.content ul { line-height: 1.45em; } +.content a { text-decoration: none; } +.content hr { max-width: var(--content-max-width); } +.content a:hover { text-decoration: underline; } +.content img, .content video { max-width: 100%; } +.content .header:link, +.content .header:visited { + color: var(--fg); +} +.content .header:link, +.content .header:visited:hover { + text-decoration: none; +} +.content .utterances { + max-width: var(--content-max-width); +} + +table { + margin: 0 auto; + border-collapse: collapse; +} +table td { + padding: 3px 20px; + border: 1px var(--table-border-color) solid; +} +table thead { + background: var(--table-header-bg); +} +table thead td { + font-weight: 700; + border: none; +} +table thead th { + padding: 3px 20px; +} +table thead tr { + border: 1px var(--table-header-bg) solid; +} +/* Alternate background colors for rows */ +table tbody tr:nth-child(2n) { + background: var(--table-alternate-bg); +} + + +blockquote { + margin: 20px 0; + padding: 0 20px; + color: var(--fg); + background-color: var(--quote-bg); + border-top: .1em solid var(--quote-border); + border-bottom: .1em solid var(--quote-border); +} + + +:not(.footnote-definition) + .footnote-definition, +.footnote-definition + :not(.footnote-definition) { + margin-top: 2em; +} +.footnote-definition { + font-size: 0.9em; + margin: 0.5em 0; +} +.footnote-definition p { + display: inline; +} + +.tooltiptext { + position: absolute; + visibility: hidden; + color: #fff; + background-color: #333; + transform: translateX(-50%); /* Center by moving tooltip 50% of its width left */ + left: -8px; /* Half of the width of the icon */ + top: -35px; + font-size: 0.8em; + text-align: center; + border-radius: 6px; + padding: 5px 8px; + margin: 5px; + z-index: 1000; +} +.tooltipped .tooltiptext { + visibility: visible; +} + +.chapter li.part-title { + color: var(--sidebar-fg); + margin: 5px 0px; + font-weight: bold; +} + +.result-no-output { + font-style: italic; +} diff --git a/css/print.css b/css/print.css new file mode 100644 index 0000000..5e690f7 --- /dev/null +++ b/css/print.css @@ -0,0 +1,54 @@ + +#sidebar, +#menu-bar, +.nav-chapters, +.mobile-nav-chapters { + display: none; +} + +#page-wrapper.page-wrapper { + transform: none; + margin-left: 0px; + overflow-y: initial; +} + +#content { + max-width: none; + margin: 0; + padding: 0; +} + +.page { + overflow-y: initial; +} + +code { + background-color: #666666; + border-radius: 5px; + + /* Force background to be printed in Chrome */ + -webkit-print-color-adjust: exact; +} + +pre > .buttons { + z-index: 2; +} + +a, a:visited, a:active, a:hover { + color: #4183c4; + text-decoration: none; +} + +h1, h2, h3, h4, h5, h6 { + page-break-inside: avoid; + page-break-after: avoid; +} + +pre, code { + page-break-inside: avoid; + white-space: pre-wrap; +} + +.fa { + display: none !important; +} diff --git a/css/variables.css b/css/variables.css new file mode 100644 index 0000000..56b634b --- /dev/null +++ b/css/variables.css @@ -0,0 +1,253 @@ + +/* Globals */ + +:root { + --sidebar-width: 300px; + --page-padding: 15px; + --content-max-width: 750px; + --menu-bar-height: 50px; +} + +/* Themes */ + +.ayu { + --bg: hsl(210, 25%, 8%); + --fg: #c5c5c5; + + --sidebar-bg: #14191f; + --sidebar-fg: #c8c9db; + --sidebar-non-existant: #5c6773; + --sidebar-active: #ffb454; + --sidebar-spacer: #2d334f; + + --scrollbar: var(--sidebar-fg); + + --icons: #737480; + --icons-hover: #b7b9cc; + + --links: #0096cf; + + --inline-code-color: #ffb454; + + --theme-popup-bg: #14191f; + --theme-popup-border: #5c6773; + --theme-hover: #191f26; + + --quote-bg: hsl(226, 15%, 17%); + --quote-border: hsl(226, 15%, 22%); + + --table-border-color: hsl(210, 25%, 13%); + --table-header-bg: hsl(210, 25%, 28%); + --table-alternate-bg: hsl(210, 25%, 11%); + + --searchbar-border-color: #848484; + --searchbar-bg: #424242; + --searchbar-fg: #fff; + --searchbar-shadow-color: #d4c89f; + --searchresults-header-fg: #666; + --searchresults-border-color: #888; + --searchresults-li-bg: #252932; + --search-mark-bg: #e3b171; +} + +.coal { + --bg: hsl(200, 7%, 8%); + --fg: #98a3ad; + + --sidebar-bg: #292c2f; + --sidebar-fg: #a1adb8; + --sidebar-non-existant: #505254; + --sidebar-active: #3473ad; + --sidebar-spacer: #393939; + + --scrollbar: var(--sidebar-fg); + + --icons: #43484d; + --icons-hover: #b3c0cc; + + --links: #2b79a2; + + --inline-code-color: #c5c8c6; + + --theme-popup-bg: #141617; + --theme-popup-border: #43484d; + --theme-hover: #1f2124; + + --quote-bg: hsl(234, 21%, 18%); + --quote-border: hsl(234, 21%, 23%); + + --table-border-color: hsl(200, 7%, 13%); + --table-header-bg: hsl(200, 7%, 28%); + --table-alternate-bg: hsl(200, 7%, 11%); + + --searchbar-border-color: #aaa; + --searchbar-bg: #b7b7b7; + --searchbar-fg: #000; + --searchbar-shadow-color: #aaa; + --searchresults-header-fg: #666; + --searchresults-border-color: #98a3ad; + --searchresults-li-bg: #2b2b2f; + --search-mark-bg: #355c7d; +} + +.light { + --bg: hsl(0, 0%, 100%); + --fg: hsl(0, 0%, 0%); + + --sidebar-bg: #fafafa; + --sidebar-fg: hsl(0, 0%, 0%); + --sidebar-non-existant: #aaaaaa; + --sidebar-active: #1f1fff; + --sidebar-spacer: #f4f4f4; + + --scrollbar: #8F8F8F; + + --icons: #747474; + --icons-hover: #000000; + + --links: #20609f; + + --inline-code-color: #301900; + + --theme-popup-bg: #fafafa; + --theme-popup-border: #cccccc; + --theme-hover: #e6e6e6; + + --quote-bg: hsl(197, 37%, 96%); + --quote-border: hsl(197, 37%, 91%); + + --table-border-color: hsl(0, 0%, 95%); + --table-header-bg: hsl(0, 0%, 80%); + --table-alternate-bg: hsl(0, 0%, 97%); + + --searchbar-border-color: #aaa; + --searchbar-bg: #fafafa; + --searchbar-fg: #000; + --searchbar-shadow-color: #aaa; + --searchresults-header-fg: #666; + --searchresults-border-color: #888; + --searchresults-li-bg: #e4f2fe; + --search-mark-bg: #a2cff5; +} + +.navy { + --bg: hsl(226, 23%, 11%); + --fg: #bcbdd0; + + --sidebar-bg: #282d3f; + --sidebar-fg: #c8c9db; + --sidebar-non-existant: #505274; + --sidebar-active: #2b79a2; + --sidebar-spacer: #2d334f; + + --scrollbar: var(--sidebar-fg); + + --icons: #737480; + --icons-hover: #b7b9cc; + + --links: #2b79a2; + + --inline-code-color: #c5c8c6; + + --theme-popup-bg: #161923; + --theme-popup-border: #737480; + --theme-hover: #282e40; + + --quote-bg: hsl(226, 15%, 17%); + --quote-border: hsl(226, 15%, 22%); + + --table-border-color: hsl(226, 23%, 16%); + --table-header-bg: hsl(226, 23%, 31%); + --table-alternate-bg: hsl(226, 23%, 14%); + + --searchbar-border-color: #aaa; + --searchbar-bg: #aeaec6; + --searchbar-fg: #000; + --searchbar-shadow-color: #aaa; + --searchresults-header-fg: #5f5f71; + --searchresults-border-color: #5c5c68; + --searchresults-li-bg: #242430; + --search-mark-bg: #a2cff5; +} + +.rust { + --bg: hsl(60, 9%, 87%); + --fg: #262625; + + --sidebar-bg: #3b2e2a; + --sidebar-fg: #c8c9db; + --sidebar-non-existant: #505254; + --sidebar-active: #e69f67; + --sidebar-spacer: #45373a; + + --scrollbar: var(--sidebar-fg); + + --icons: #737480; + --icons-hover: #262625; + + --links: #2b79a2; + + --inline-code-color: #6e6b5e; + + --theme-popup-bg: #e1e1db; + --theme-popup-border: #b38f6b; + --theme-hover: #99908a; + + --quote-bg: hsl(60, 5%, 75%); + --quote-border: hsl(60, 5%, 70%); + + --table-border-color: hsl(60, 9%, 82%); + --table-header-bg: #b3a497; + --table-alternate-bg: hsl(60, 9%, 84%); + + --searchbar-border-color: #aaa; + --searchbar-bg: #fafafa; + --searchbar-fg: #000; + --searchbar-shadow-color: #aaa; + --searchresults-header-fg: #666; + --searchresults-border-color: #888; + --searchresults-li-bg: #dec2a2; + --search-mark-bg: #e69f67; +} + +@media (prefers-color-scheme: dark) { + .light.no-js { + --bg: hsl(200, 7%, 8%); + --fg: #98a3ad; + + --sidebar-bg: #292c2f; + --sidebar-fg: #a1adb8; + --sidebar-non-existant: #505254; + --sidebar-active: #3473ad; + --sidebar-spacer: #393939; + + --scrollbar: var(--sidebar-fg); + + --icons: #43484d; + --icons-hover: #b3c0cc; + + --links: #2b79a2; + + --inline-code-color: #c5c8c6; + + --theme-popup-bg: #141617; + --theme-popup-border: #43484d; + --theme-hover: #1f2124; + + --quote-bg: hsl(234, 21%, 18%); + --quote-border: hsl(234, 21%, 23%); + + --table-border-color: hsl(200, 7%, 13%); + --table-header-bg: hsl(200, 7%, 28%); + --table-alternate-bg: hsl(200, 7%, 11%); + + --searchbar-border-color: #aaa; + --searchbar-bg: #b7b7b7; + --searchbar-fg: #000; + --searchbar-shadow-color: #aaa; + --searchresults-header-fg: #666; + --searchresults-border-color: #98a3ad; + --searchresults-li-bg: #2b2b2f; + --search-mark-bg: #355c7d; + } +} diff --git a/elasticlunr.min.js b/elasticlunr.min.js new file mode 100644 index 0000000..94b20dd --- /dev/null +++ b/elasticlunr.min.js @@ -0,0 +1,10 @@ +/** + * elasticlunr - http://weixsong.github.io + * Lightweight full-text search engine in Javascript for browser search and offline search. - 0.9.5 + * + * Copyright (C) 2017 Oliver Nightingale + * Copyright (C) 2017 Wei Song + * MIT Licensed + * @license + */ +!function(){function e(e){if(null===e||"object"!=typeof e)return e;var t=e.constructor();for(var n in e)e.hasOwnProperty(n)&&(t[n]=e[n]);return t}var t=function(e){var n=new t.Index;return n.pipeline.add(t.trimmer,t.stopWordFilter,t.stemmer),e&&e.call(n,n),n};t.version="0.9.5",lunr=t,t.utils={},t.utils.warn=function(e){return function(t){e.console&&console.warn&&console.warn(t)}}(this),t.utils.toString=function(e){return void 0===e||null===e?"":e.toString()},t.EventEmitter=function(){this.events={}},t.EventEmitter.prototype.addListener=function(){var e=Array.prototype.slice.call(arguments),t=e.pop(),n=e;if("function"!=typeof t)throw new TypeError("last argument must be a function");n.forEach(function(e){this.hasHandler(e)||(this.events[e]=[]),this.events[e].push(t)},this)},t.EventEmitter.prototype.removeListener=function(e,t){if(this.hasHandler(e)){var n=this.events[e].indexOf(t);-1!==n&&(this.events[e].splice(n,1),0==this.events[e].length&&delete this.events[e])}},t.EventEmitter.prototype.emit=function(e){if(this.hasHandler(e)){var t=Array.prototype.slice.call(arguments,1);this.events[e].forEach(function(e){e.apply(void 0,t)},this)}},t.EventEmitter.prototype.hasHandler=function(e){return e in this.events},t.tokenizer=function(e){if(!arguments.length||null===e||void 0===e)return[];if(Array.isArray(e)){var n=e.filter(function(e){return null===e||void 0===e?!1:!0});n=n.map(function(e){return t.utils.toString(e).toLowerCase()});var i=[];return n.forEach(function(e){var n=e.split(t.tokenizer.seperator);i=i.concat(n)},this),i}return e.toString().trim().toLowerCase().split(t.tokenizer.seperator)},t.tokenizer.defaultSeperator=/[\s\-]+/,t.tokenizer.seperator=t.tokenizer.defaultSeperator,t.tokenizer.setSeperator=function(e){null!==e&&void 0!==e&&"object"==typeof e&&(t.tokenizer.seperator=e)},t.tokenizer.resetSeperator=function(){t.tokenizer.seperator=t.tokenizer.defaultSeperator},t.tokenizer.getSeperator=function(){return t.tokenizer.seperator},t.Pipeline=function(){this._queue=[]},t.Pipeline.registeredFunctions={},t.Pipeline.registerFunction=function(e,n){n in t.Pipeline.registeredFunctions&&t.utils.warn("Overwriting existing registered function: "+n),e.label=n,t.Pipeline.registeredFunctions[n]=e},t.Pipeline.getRegisteredFunction=function(e){return e in t.Pipeline.registeredFunctions!=!0?null:t.Pipeline.registeredFunctions[e]},t.Pipeline.warnIfFunctionNotRegistered=function(e){var n=e.label&&e.label in this.registeredFunctions;n||t.utils.warn("Function is not registered with pipeline. This may cause problems when serialising the index.\n",e)},t.Pipeline.load=function(e){var n=new t.Pipeline;return e.forEach(function(e){var i=t.Pipeline.getRegisteredFunction(e);if(!i)throw new Error("Cannot load un-registered function: "+e);n.add(i)}),n},t.Pipeline.prototype.add=function(){var e=Array.prototype.slice.call(arguments);e.forEach(function(e){t.Pipeline.warnIfFunctionNotRegistered(e),this._queue.push(e)},this)},t.Pipeline.prototype.after=function(e,n){t.Pipeline.warnIfFunctionNotRegistered(n);var i=this._queue.indexOf(e);if(-1===i)throw new Error("Cannot find existingFn");this._queue.splice(i+1,0,n)},t.Pipeline.prototype.before=function(e,n){t.Pipeline.warnIfFunctionNotRegistered(n);var i=this._queue.indexOf(e);if(-1===i)throw new Error("Cannot find existingFn");this._queue.splice(i,0,n)},t.Pipeline.prototype.remove=function(e){var t=this._queue.indexOf(e);-1!==t&&this._queue.splice(t,1)},t.Pipeline.prototype.run=function(e){for(var t=[],n=e.length,i=this._queue.length,o=0;n>o;o++){for(var r=e[o],s=0;i>s&&(r=this._queue[s](r,o,e),void 0!==r&&null!==r);s++);void 0!==r&&null!==r&&t.push(r)}return t},t.Pipeline.prototype.reset=function(){this._queue=[]},t.Pipeline.prototype.get=function(){return this._queue},t.Pipeline.prototype.toJSON=function(){return this._queue.map(function(e){return t.Pipeline.warnIfFunctionNotRegistered(e),e.label})},t.Index=function(){this._fields=[],this._ref="id",this.pipeline=new t.Pipeline,this.documentStore=new t.DocumentStore,this.index={},this.eventEmitter=new t.EventEmitter,this._idfCache={},this.on("add","remove","update",function(){this._idfCache={}}.bind(this))},t.Index.prototype.on=function(){var e=Array.prototype.slice.call(arguments);return this.eventEmitter.addListener.apply(this.eventEmitter,e)},t.Index.prototype.off=function(e,t){return this.eventEmitter.removeListener(e,t)},t.Index.load=function(e){e.version!==t.version&&t.utils.warn("version mismatch: current "+t.version+" importing "+e.version);var n=new this;n._fields=e.fields,n._ref=e.ref,n.documentStore=t.DocumentStore.load(e.documentStore),n.pipeline=t.Pipeline.load(e.pipeline),n.index={};for(var i in e.index)n.index[i]=t.InvertedIndex.load(e.index[i]);return n},t.Index.prototype.addField=function(e){return this._fields.push(e),this.index[e]=new t.InvertedIndex,this},t.Index.prototype.setRef=function(e){return this._ref=e,this},t.Index.prototype.saveDocument=function(e){return this.documentStore=new t.DocumentStore(e),this},t.Index.prototype.addDoc=function(e,n){if(e){var n=void 0===n?!0:n,i=e[this._ref];this.documentStore.addDoc(i,e),this._fields.forEach(function(n){var o=this.pipeline.run(t.tokenizer(e[n]));this.documentStore.addFieldLength(i,n,o.length);var r={};o.forEach(function(e){e in r?r[e]+=1:r[e]=1},this);for(var s in r){var u=r[s];u=Math.sqrt(u),this.index[n].addToken(s,{ref:i,tf:u})}},this),n&&this.eventEmitter.emit("add",e,this)}},t.Index.prototype.removeDocByRef=function(e){if(e&&this.documentStore.isDocStored()!==!1&&this.documentStore.hasDoc(e)){var t=this.documentStore.getDoc(e);this.removeDoc(t,!1)}},t.Index.prototype.removeDoc=function(e,n){if(e){var n=void 0===n?!0:n,i=e[this._ref];this.documentStore.hasDoc(i)&&(this.documentStore.removeDoc(i),this._fields.forEach(function(n){var o=this.pipeline.run(t.tokenizer(e[n]));o.forEach(function(e){this.index[n].removeToken(e,i)},this)},this),n&&this.eventEmitter.emit("remove",e,this))}},t.Index.prototype.updateDoc=function(e,t){var t=void 0===t?!0:t;this.removeDocByRef(e[this._ref],!1),this.addDoc(e,!1),t&&this.eventEmitter.emit("update",e,this)},t.Index.prototype.idf=function(e,t){var n="@"+t+"/"+e;if(Object.prototype.hasOwnProperty.call(this._idfCache,n))return this._idfCache[n];var i=this.index[t].getDocFreq(e),o=1+Math.log(this.documentStore.length/(i+1));return this._idfCache[n]=o,o},t.Index.prototype.getFields=function(){return this._fields.slice()},t.Index.prototype.search=function(e,n){if(!e)return[];e="string"==typeof e?{any:e}:JSON.parse(JSON.stringify(e));var i=null;null!=n&&(i=JSON.stringify(n));for(var o=new t.Configuration(i,this.getFields()).get(),r={},s=Object.keys(e),u=0;u0&&t.push(e);for(var i in n)"docs"!==i&&"df"!==i&&this.expandToken(e+i,t,n[i]);return t},t.InvertedIndex.prototype.toJSON=function(){return{root:this.root}},t.Configuration=function(e,n){var e=e||"";if(void 0==n||null==n)throw new Error("fields should not be null");this.config={};var i;try{i=JSON.parse(e),this.buildUserConfig(i,n)}catch(o){t.utils.warn("user configuration parse failed, will use default configuration"),this.buildDefaultConfig(n)}},t.Configuration.prototype.buildDefaultConfig=function(e){this.reset(),e.forEach(function(e){this.config[e]={boost:1,bool:"OR",expand:!1}},this)},t.Configuration.prototype.buildUserConfig=function(e,n){var i="OR",o=!1;if(this.reset(),"bool"in e&&(i=e.bool||i),"expand"in e&&(o=e.expand||o),"fields"in e)for(var r in e.fields)if(n.indexOf(r)>-1){var s=e.fields[r],u=o;void 0!=s.expand&&(u=s.expand),this.config[r]={boost:s.boost||0===s.boost?s.boost:1,bool:s.bool||i,expand:u}}else t.utils.warn("field name in user configuration not found in index instance fields");else this.addAllFields2UserConfig(i,o,n)},t.Configuration.prototype.addAllFields2UserConfig=function(e,t,n){n.forEach(function(n){this.config[n]={boost:1,bool:e,expand:t}},this)},t.Configuration.prototype.get=function(){return this.config},t.Configuration.prototype.reset=function(){this.config={}},lunr.SortedSet=function(){this.length=0,this.elements=[]},lunr.SortedSet.load=function(e){var t=new this;return t.elements=e,t.length=e.length,t},lunr.SortedSet.prototype.add=function(){var e,t;for(e=0;e1;){if(r===e)return o;e>r&&(t=o),r>e&&(n=o),i=n-t,o=t+Math.floor(i/2),r=this.elements[o]}return r===e?o:-1},lunr.SortedSet.prototype.locationFor=function(e){for(var t=0,n=this.elements.length,i=n-t,o=t+Math.floor(i/2),r=this.elements[o];i>1;)e>r&&(t=o),r>e&&(n=o),i=n-t,o=t+Math.floor(i/2),r=this.elements[o];return r>e?o:e>r?o+1:void 0},lunr.SortedSet.prototype.intersect=function(e){for(var t=new lunr.SortedSet,n=0,i=0,o=this.length,r=e.length,s=this.elements,u=e.elements;;){if(n>o-1||i>r-1)break;s[n]!==u[i]?s[n]u[i]&&i++:(t.add(s[n]),n++,i++)}return t},lunr.SortedSet.prototype.clone=function(){var e=new lunr.SortedSet;return e.elements=this.toArray(),e.length=e.elements.length,e},lunr.SortedSet.prototype.union=function(e){var t,n,i;this.length>=e.length?(t=this,n=e):(t=e,n=this),i=t.clone();for(var o=0,r=n.toArray();o + + + + diff --git a/fonts/OPEN-SANS-LICENSE.txt b/fonts/OPEN-SANS-LICENSE.txt new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/fonts/OPEN-SANS-LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/fonts/SOURCE-CODE-PRO-LICENSE.txt b/fonts/SOURCE-CODE-PRO-LICENSE.txt new file mode 100644 index 0000000..366206f --- /dev/null +++ b/fonts/SOURCE-CODE-PRO-LICENSE.txt @@ -0,0 +1,93 @@ +Copyright 2010, 2012 Adobe Systems Incorporated (http://www.adobe.com/), with Reserved Font Name 'Source'. All Rights Reserved. Source is a trademark of Adobe Systems Incorporated in the United States and/or other countries. + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/fonts/fonts.css b/fonts/fonts.css new file mode 100644 index 0000000..858efa5 --- /dev/null +++ b/fonts/fonts.css @@ -0,0 +1,100 @@ +/* Open Sans is licensed under the Apache License, Version 2.0. See http://www.apache.org/licenses/LICENSE-2.0 */ +/* Source Code Pro is under the Open Font License. See https://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=OFL */ + +/* open-sans-300 - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 300; + src: local('Open Sans Light'), local('OpenSans-Light'), + url('open-sans-v17-all-charsets-300.woff2') format('woff2'); +} + +/* open-sans-300italic - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */ +@font-face { + font-family: 'Open Sans'; + font-style: italic; + font-weight: 300; + src: local('Open Sans Light Italic'), local('OpenSans-LightItalic'), + url('open-sans-v17-all-charsets-300italic.woff2') format('woff2'); +} + +/* open-sans-regular - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 400; + src: local('Open Sans Regular'), local('OpenSans-Regular'), + url('open-sans-v17-all-charsets-regular.woff2') format('woff2'); +} + +/* open-sans-italic - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */ +@font-face { + font-family: 'Open Sans'; + font-style: italic; + font-weight: 400; + src: local('Open Sans Italic'), local('OpenSans-Italic'), + url('open-sans-v17-all-charsets-italic.woff2') format('woff2'); +} + +/* open-sans-600 - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 600; + src: local('Open Sans SemiBold'), local('OpenSans-SemiBold'), + url('open-sans-v17-all-charsets-600.woff2') format('woff2'); +} + +/* open-sans-600italic - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */ +@font-face { + font-family: 'Open Sans'; + font-style: italic; + font-weight: 600; + src: local('Open Sans SemiBold Italic'), local('OpenSans-SemiBoldItalic'), + url('open-sans-v17-all-charsets-600italic.woff2') format('woff2'); +} + +/* open-sans-700 - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 700; + src: local('Open Sans Bold'), local('OpenSans-Bold'), + url('open-sans-v17-all-charsets-700.woff2') format('woff2'); +} + +/* open-sans-700italic - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */ +@font-face { + font-family: 'Open Sans'; + font-style: italic; + font-weight: 700; + src: local('Open Sans Bold Italic'), local('OpenSans-BoldItalic'), + url('open-sans-v17-all-charsets-700italic.woff2') format('woff2'); +} + +/* open-sans-800 - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 800; + src: local('Open Sans ExtraBold'), local('OpenSans-ExtraBold'), + url('open-sans-v17-all-charsets-800.woff2') format('woff2'); +} + +/* open-sans-800italic - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */ +@font-face { + font-family: 'Open Sans'; + font-style: italic; + font-weight: 800; + src: local('Open Sans ExtraBold Italic'), local('OpenSans-ExtraBoldItalic'), + url('open-sans-v17-all-charsets-800italic.woff2') format('woff2'); +} + +/* source-code-pro-500 - latin_vietnamese_latin-ext_greek_cyrillic-ext_cyrillic */ +@font-face { + font-family: 'Source Code Pro'; + font-style: normal; + font-weight: 500; + src: url('source-code-pro-v11-all-charsets-500.woff2') format('woff2'); +} diff --git a/fonts/open-sans-v17-all-charsets-300.woff2 b/fonts/open-sans-v17-all-charsets-300.woff2 new file mode 100644 index 0000000..9f51be3 Binary files /dev/null and b/fonts/open-sans-v17-all-charsets-300.woff2 differ diff --git a/fonts/open-sans-v17-all-charsets-300italic.woff2 b/fonts/open-sans-v17-all-charsets-300italic.woff2 new file mode 100644 index 0000000..2f54544 Binary files /dev/null and b/fonts/open-sans-v17-all-charsets-300italic.woff2 differ diff --git a/fonts/open-sans-v17-all-charsets-600.woff2 b/fonts/open-sans-v17-all-charsets-600.woff2 new file mode 100644 index 0000000..f503d55 Binary files /dev/null and b/fonts/open-sans-v17-all-charsets-600.woff2 differ diff --git a/fonts/open-sans-v17-all-charsets-600italic.woff2 b/fonts/open-sans-v17-all-charsets-600italic.woff2 new file mode 100644 index 0000000..c99aabe Binary files /dev/null and b/fonts/open-sans-v17-all-charsets-600italic.woff2 differ diff --git a/fonts/open-sans-v17-all-charsets-700.woff2 b/fonts/open-sans-v17-all-charsets-700.woff2 new file mode 100644 index 0000000..421a1ab Binary files /dev/null and b/fonts/open-sans-v17-all-charsets-700.woff2 differ diff --git a/fonts/open-sans-v17-all-charsets-700italic.woff2 b/fonts/open-sans-v17-all-charsets-700italic.woff2 new file mode 100644 index 0000000..12ce3d2 Binary files /dev/null and b/fonts/open-sans-v17-all-charsets-700italic.woff2 differ diff --git a/fonts/open-sans-v17-all-charsets-800.woff2 b/fonts/open-sans-v17-all-charsets-800.woff2 new file mode 100644 index 0000000..c94a223 Binary files /dev/null and b/fonts/open-sans-v17-all-charsets-800.woff2 differ diff --git a/fonts/open-sans-v17-all-charsets-800italic.woff2 b/fonts/open-sans-v17-all-charsets-800italic.woff2 new file mode 100644 index 0000000..eed7d3c Binary files /dev/null and b/fonts/open-sans-v17-all-charsets-800italic.woff2 differ diff --git a/fonts/open-sans-v17-all-charsets-italic.woff2 b/fonts/open-sans-v17-all-charsets-italic.woff2 new file mode 100644 index 0000000..398b68a Binary files /dev/null and b/fonts/open-sans-v17-all-charsets-italic.woff2 differ diff --git a/fonts/open-sans-v17-all-charsets-regular.woff2 b/fonts/open-sans-v17-all-charsets-regular.woff2 new file mode 100644 index 0000000..8383e94 Binary files /dev/null and b/fonts/open-sans-v17-all-charsets-regular.woff2 differ diff --git a/fonts/source-code-pro-v11-all-charsets-500.woff2 b/fonts/source-code-pro-v11-all-charsets-500.woff2 new file mode 100644 index 0000000..7222456 Binary files /dev/null and b/fonts/source-code-pro-v11-all-charsets-500.woff2 differ diff --git a/highlight.css b/highlight.css new file mode 100644 index 0000000..c234322 --- /dev/null +++ b/highlight.css @@ -0,0 +1,83 @@ +/* + * An increased contrast highlighting scheme loosely based on the + * "Base16 Atelier Dune Light" theme by Bram de Haan + * (http://atelierbram.github.io/syntax-highlighting/atelier-schemes/dune) + * Original Base16 color scheme by Chris Kempson + * (https://github.com/chriskempson/base16) + */ + +/* Comment */ +.hljs-comment, +.hljs-quote { + color: #575757; +} + +/* Red */ +.hljs-variable, +.hljs-template-variable, +.hljs-attribute, +.hljs-tag, +.hljs-name, +.hljs-regexp, +.hljs-link, +.hljs-name, +.hljs-selector-id, +.hljs-selector-class { + color: #d70025; +} + +/* Orange */ +.hljs-number, +.hljs-meta, +.hljs-built_in, +.hljs-builtin-name, +.hljs-literal, +.hljs-type, +.hljs-params { + color: #b21e00; +} + +/* Green */ +.hljs-string, +.hljs-symbol, +.hljs-bullet { + color: #008200; +} + +/* Blue */ +.hljs-title, +.hljs-section { + color: #0030f2; +} + +/* Purple */ +.hljs-keyword, +.hljs-selector-tag { + color: #9d00ec; +} + +.hljs { + display: block; + overflow-x: auto; + background: #f6f7f6; + color: #000; + padding: 0.5em; +} + +.hljs-emphasis { + font-style: italic; +} + +.hljs-strong { + font-weight: bold; +} + +.hljs-addition { + color: #22863a; + background-color: #f0fff4; +} + +.hljs-deletion { + color: #b31d28; + background-color: #ffeef0; +} diff --git a/highlight.js b/highlight.js new file mode 100644 index 0000000..180385b --- /dev/null +++ b/highlight.js @@ -0,0 +1,6 @@ +/* + Highlight.js 10.1.1 (93fd0d73) + License: BSD-3-Clause + Copyright (c) 2006-2020, Ivan Sagalaev +*/ +var hljs=function(){"use strict";function e(n){Object.freeze(n);var t="function"==typeof n;return Object.getOwnPropertyNames(n).forEach((function(r){!Object.hasOwnProperty.call(n,r)||null===n[r]||"object"!=typeof n[r]&&"function"!=typeof n[r]||t&&("caller"===r||"callee"===r||"arguments"===r)||Object.isFrozen(n[r])||e(n[r])})),n}class n{constructor(e){void 0===e.data&&(e.data={}),this.data=e.data}ignoreMatch(){this.ignore=!0}}function t(e){return e.replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'")}function r(e,...n){var t={};for(const n in e)t[n]=e[n];return n.forEach((function(e){for(const n in e)t[n]=e[n]})),t}function a(e){return e.nodeName.toLowerCase()}var i=Object.freeze({__proto__:null,escapeHTML:t,inherit:r,nodeStream:function(e){var n=[];return function e(t,r){for(var i=t.firstChild;i;i=i.nextSibling)3===i.nodeType?r+=i.nodeValue.length:1===i.nodeType&&(n.push({event:"start",offset:r,node:i}),r=e(i,r),a(i).match(/br|hr|img|input/)||n.push({event:"stop",offset:r,node:i}));return r}(e,0),n},mergeStreams:function(e,n,r){var i=0,s="",o=[];function l(){return e.length&&n.length?e[0].offset!==n[0].offset?e[0].offset"}function u(e){s+=""}function d(e){("start"===e.event?c:u)(e.node)}for(;e.length||n.length;){var g=l();if(s+=t(r.substring(i,g[0].offset)),i=g[0].offset,g===e){o.reverse().forEach(u);do{d(g.splice(0,1)[0]),g=l()}while(g===e&&g.length&&g[0].offset===i);o.reverse().forEach(c)}else"start"===g[0].event?o.push(g[0].node):o.pop(),d(g.splice(0,1)[0])}return s+t(r.substr(i))}});const s="",o=e=>!!e.kind;class l{constructor(e,n){this.buffer="",this.classPrefix=n.classPrefix,e.walk(this)}addText(e){this.buffer+=t(e)}openNode(e){if(!o(e))return;let n=e.kind;e.sublanguage||(n=`${this.classPrefix}${n}`),this.span(n)}closeNode(e){o(e)&&(this.buffer+=s)}value(){return this.buffer}span(e){this.buffer+=``}}class c{constructor(){this.rootNode={children:[]},this.stack=[this.rootNode]}get top(){return this.stack[this.stack.length-1]}get root(){return this.rootNode}add(e){this.top.children.push(e)}openNode(e){const n={kind:e,children:[]};this.add(n),this.stack.push(n)}closeNode(){if(this.stack.length>1)return this.stack.pop()}closeAllNodes(){for(;this.closeNode(););}toJSON(){return JSON.stringify(this.rootNode,null,4)}walk(e){return this.constructor._walk(e,this.rootNode)}static _walk(e,n){return"string"==typeof n?e.addText(n):n.children&&(e.openNode(n),n.children.forEach(n=>this._walk(e,n)),e.closeNode(n)),e}static _collapse(e){"string"!=typeof e&&e.children&&(e.children.every(e=>"string"==typeof e)?e.children=[e.children.join("")]:e.children.forEach(e=>{c._collapse(e)}))}}class u extends c{constructor(e){super(),this.options=e}addKeyword(e,n){""!==e&&(this.openNode(n),this.addText(e),this.closeNode())}addText(e){""!==e&&this.add(e)}addSublanguage(e,n){const t=e.root;t.kind=n,t.sublanguage=!0,this.add(t)}toHTML(){return new l(this,this.options).value()}finalize(){return!0}}function d(e){return e?"string"==typeof e?e:e.source:null}const g="(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",h={begin:"\\\\[\\s\\S]",relevance:0},f={className:"string",begin:"'",end:"'",illegal:"\\n",contains:[h]},p={className:"string",begin:'"',end:'"',illegal:"\\n",contains:[h]},b={begin:/\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|they|like|more)\b/},m=function(e,n,t={}){var a=r({className:"comment",begin:e,end:n,contains:[]},t);return a.contains.push(b),a.contains.push({className:"doctag",begin:"(?:TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):",relevance:0}),a},v=m("//","$"),x=m("/\\*","\\*/"),E=m("#","$");var _=Object.freeze({__proto__:null,IDENT_RE:"[a-zA-Z]\\w*",UNDERSCORE_IDENT_RE:"[a-zA-Z_]\\w*",NUMBER_RE:"\\b\\d+(\\.\\d+)?",C_NUMBER_RE:g,BINARY_NUMBER_RE:"\\b(0b[01]+)",RE_STARTERS_RE:"!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~",SHEBANG:(e={})=>{const n=/^#![ ]*\//;return e.binary&&(e.begin=function(...e){return e.map(e=>d(e)).join("")}(n,/.*\b/,e.binary,/\b.*/)),r({className:"meta",begin:n,end:/$/,relevance:0,"on:begin":(e,n)=>{0!==e.index&&n.ignoreMatch()}},e)},BACKSLASH_ESCAPE:h,APOS_STRING_MODE:f,QUOTE_STRING_MODE:p,PHRASAL_WORDS_MODE:b,COMMENT:m,C_LINE_COMMENT_MODE:v,C_BLOCK_COMMENT_MODE:x,HASH_COMMENT_MODE:E,NUMBER_MODE:{className:"number",begin:"\\b\\d+(\\.\\d+)?",relevance:0},C_NUMBER_MODE:{className:"number",begin:g,relevance:0},BINARY_NUMBER_MODE:{className:"number",begin:"\\b(0b[01]+)",relevance:0},CSS_NUMBER_MODE:{className:"number",begin:"\\b\\d+(\\.\\d+)?(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?",relevance:0},REGEXP_MODE:{begin:/(?=\/[^/\n]*\/)/,contains:[{className:"regexp",begin:/\//,end:/\/[gimuy]*/,illegal:/\n/,contains:[h,{begin:/\[/,end:/\]/,relevance:0,contains:[h]}]}]},TITLE_MODE:{className:"title",begin:"[a-zA-Z]\\w*",relevance:0},UNDERSCORE_TITLE_MODE:{className:"title",begin:"[a-zA-Z_]\\w*",relevance:0},METHOD_GUARD:{begin:"\\.\\s*[a-zA-Z_]\\w*",relevance:0},END_SAME_AS_BEGIN:function(e){return Object.assign(e,{"on:begin":(e,n)=>{n.data._beginMatch=e[1]},"on:end":(e,n)=>{n.data._beginMatch!==e[1]&&n.ignoreMatch()}})}}),N="of and for in not or if then".split(" ");function w(e,n){return n?+n:function(e){return N.includes(e.toLowerCase())}(e)?0:1}const R=t,y=r,{nodeStream:k,mergeStreams:O}=i,M=Symbol("nomatch");return function(t){var a=[],i={},s={},o=[],l=!0,c=/(^(<[^>]+>|\t|)+|\n)/gm,g="Could not find the language '{}', did you forget to load/include a language module?";const h={disableAutodetect:!0,name:"Plain text",contains:[]};var f={noHighlightRe:/^(no-?highlight)$/i,languageDetectRe:/\blang(?:uage)?-([\w-]+)\b/i,classPrefix:"hljs-",tabReplace:null,useBR:!1,languages:null,__emitter:u};function p(e){return f.noHighlightRe.test(e)}function b(e,n,t,r){var a={code:n,language:e};S("before:highlight",a);var i=a.result?a.result:m(a.language,a.code,t,r);return i.code=a.code,S("after:highlight",i),i}function m(e,t,a,s){var o=t;function c(e,n){var t=E.case_insensitive?n[0].toLowerCase():n[0];return Object.prototype.hasOwnProperty.call(e.keywords,t)&&e.keywords[t]}function u(){null!=y.subLanguage?function(){if(""!==A){var e=null;if("string"==typeof y.subLanguage){if(!i[y.subLanguage])return void O.addText(A);e=m(y.subLanguage,A,!0,k[y.subLanguage]),k[y.subLanguage]=e.top}else e=v(A,y.subLanguage.length?y.subLanguage:null);y.relevance>0&&(I+=e.relevance),O.addSublanguage(e.emitter,e.language)}}():function(){if(!y.keywords)return void O.addText(A);let e=0;y.keywordPatternRe.lastIndex=0;let n=y.keywordPatternRe.exec(A),t="";for(;n;){t+=A.substring(e,n.index);const r=c(y,n);if(r){const[e,a]=r;O.addText(t),t="",I+=a,O.addKeyword(n[0],e)}else t+=n[0];e=y.keywordPatternRe.lastIndex,n=y.keywordPatternRe.exec(A)}t+=A.substr(e),O.addText(t)}(),A=""}function h(e){return e.className&&O.openNode(e.className),y=Object.create(e,{parent:{value:y}})}function p(e){return 0===y.matcher.regexIndex?(A+=e[0],1):(L=!0,0)}var b={};function x(t,r){var i=r&&r[0];if(A+=t,null==i)return u(),0;if("begin"===b.type&&"end"===r.type&&b.index===r.index&&""===i){if(A+=o.slice(r.index,r.index+1),!l){const n=Error("0 width match regex");throw n.languageName=e,n.badRule=b.rule,n}return 1}if(b=r,"begin"===r.type)return function(e){var t=e[0],r=e.rule;const a=new n(r),i=[r.__beforeBegin,r["on:begin"]];for(const n of i)if(n&&(n(e,a),a.ignore))return p(t);return r&&r.endSameAsBegin&&(r.endRe=RegExp(t.replace(/[-/\\^$*+?.()|[\]{}]/g,"\\$&"),"m")),r.skip?A+=t:(r.excludeBegin&&(A+=t),u(),r.returnBegin||r.excludeBegin||(A=t)),h(r),r.returnBegin?0:t.length}(r);if("illegal"===r.type&&!a){const e=Error('Illegal lexeme "'+i+'" for mode "'+(y.className||"")+'"');throw e.mode=y,e}if("end"===r.type){var s=function(e){var t=e[0],r=o.substr(e.index),a=function e(t,r,a){let i=function(e,n){var t=e&&e.exec(n);return t&&0===t.index}(t.endRe,a);if(i){if(t["on:end"]){const e=new n(t);t["on:end"](r,e),e.ignore&&(i=!1)}if(i){for(;t.endsParent&&t.parent;)t=t.parent;return t}}if(t.endsWithParent)return e(t.parent,r,a)}(y,e,r);if(!a)return M;var i=y;i.skip?A+=t:(i.returnEnd||i.excludeEnd||(A+=t),u(),i.excludeEnd&&(A=t));do{y.className&&O.closeNode(),y.skip||y.subLanguage||(I+=y.relevance),y=y.parent}while(y!==a.parent);return a.starts&&(a.endSameAsBegin&&(a.starts.endRe=a.endRe),h(a.starts)),i.returnEnd?0:t.length}(r);if(s!==M)return s}if("illegal"===r.type&&""===i)return 1;if(B>1e5&&B>3*r.index)throw Error("potential infinite loop, way more iterations than matches");return A+=i,i.length}var E=T(e);if(!E)throw console.error(g.replace("{}",e)),Error('Unknown language: "'+e+'"');var _=function(e){function n(n,t){return RegExp(d(n),"m"+(e.case_insensitive?"i":"")+(t?"g":""))}class t{constructor(){this.matchIndexes={},this.regexes=[],this.matchAt=1,this.position=0}addRule(e,n){n.position=this.position++,this.matchIndexes[this.matchAt]=n,this.regexes.push([n,e]),this.matchAt+=function(e){return RegExp(e.toString()+"|").exec("").length-1}(e)+1}compile(){0===this.regexes.length&&(this.exec=()=>null);const e=this.regexes.map(e=>e[1]);this.matcherRe=n(function(e,n="|"){for(var t=/\[(?:[^\\\]]|\\.)*\]|\(\??|\\([1-9][0-9]*)|\\./,r=0,a="",i=0;i0&&(a+=n),a+="(";o.length>0;){var l=t.exec(o);if(null==l){a+=o;break}a+=o.substring(0,l.index),o=o.substring(l.index+l[0].length),"\\"===l[0][0]&&l[1]?a+="\\"+(+l[1]+s):(a+=l[0],"("===l[0]&&r++)}a+=")"}return a}(e),!0),this.lastIndex=0}exec(e){this.matcherRe.lastIndex=this.lastIndex;const n=this.matcherRe.exec(e);if(!n)return null;const t=n.findIndex((e,n)=>n>0&&void 0!==e),r=this.matchIndexes[t];return n.splice(0,t),Object.assign(n,r)}}class a{constructor(){this.rules=[],this.multiRegexes=[],this.count=0,this.lastIndex=0,this.regexIndex=0}getMatcher(e){if(this.multiRegexes[e])return this.multiRegexes[e];const n=new t;return this.rules.slice(e).forEach(([e,t])=>n.addRule(e,t)),n.compile(),this.multiRegexes[e]=n,n}considerAll(){this.regexIndex=0}addRule(e,n){this.rules.push([e,n]),"begin"===n.type&&this.count++}exec(e){const n=this.getMatcher(this.regexIndex);n.lastIndex=this.lastIndex;const t=n.exec(e);return t&&(this.regexIndex+=t.position+1,this.regexIndex===this.count&&(this.regexIndex=0)),t}}function i(e,n){const t=e.input[e.index-1],r=e.input[e.index+e[0].length];"."!==t&&"."!==r||n.ignoreMatch()}if(e.contains&&e.contains.includes("self"))throw Error("ERR: contains `self` is not supported at the top-level of a language. See documentation.");return function t(s,o){const l=s;if(s.compiled)return l;s.compiled=!0,s.__beforeBegin=null,s.keywords=s.keywords||s.beginKeywords;let c=null;if("object"==typeof s.keywords&&(c=s.keywords.$pattern,delete s.keywords.$pattern),s.keywords&&(s.keywords=function(e,n){var t={};return"string"==typeof e?r("keyword",e):Object.keys(e).forEach((function(n){r(n,e[n])})),t;function r(e,r){n&&(r=r.toLowerCase()),r.split(" ").forEach((function(n){var r=n.split("|");t[r[0]]=[e,w(r[0],r[1])]}))}}(s.keywords,e.case_insensitive)),s.lexemes&&c)throw Error("ERR: Prefer `keywords.$pattern` to `mode.lexemes`, BOTH are not allowed. (see mode reference) ");return l.keywordPatternRe=n(s.lexemes||c||/\w+/,!0),o&&(s.beginKeywords&&(s.begin="\\b("+s.beginKeywords.split(" ").join("|")+")(?=\\b|\\s)",s.__beforeBegin=i),s.begin||(s.begin=/\B|\b/),l.beginRe=n(s.begin),s.endSameAsBegin&&(s.end=s.begin),s.end||s.endsWithParent||(s.end=/\B|\b/),s.end&&(l.endRe=n(s.end)),l.terminator_end=d(s.end)||"",s.endsWithParent&&o.terminator_end&&(l.terminator_end+=(s.end?"|":"")+o.terminator_end)),s.illegal&&(l.illegalRe=n(s.illegal)),void 0===s.relevance&&(s.relevance=1),s.contains||(s.contains=[]),s.contains=[].concat(...s.contains.map((function(e){return function(e){return e.variants&&!e.cached_variants&&(e.cached_variants=e.variants.map((function(n){return r(e,{variants:null},n)}))),e.cached_variants?e.cached_variants:function e(n){return!!n&&(n.endsWithParent||e(n.starts))}(e)?r(e,{starts:e.starts?r(e.starts):null}):Object.isFrozen(e)?r(e):e}("self"===e?s:e)}))),s.contains.forEach((function(e){t(e,l)})),s.starts&&t(s.starts,o),l.matcher=function(e){const n=new a;return e.contains.forEach(e=>n.addRule(e.begin,{rule:e,type:"begin"})),e.terminator_end&&n.addRule(e.terminator_end,{type:"end"}),e.illegal&&n.addRule(e.illegal,{type:"illegal"}),n}(l),l}(e)}(E),N="",y=s||_,k={},O=new f.__emitter(f);!function(){for(var e=[],n=y;n!==E;n=n.parent)n.className&&e.unshift(n.className);e.forEach(e=>O.openNode(e))}();var A="",I=0,S=0,B=0,L=!1;try{for(y.matcher.considerAll();;){B++,L?L=!1:(y.matcher.lastIndex=S,y.matcher.considerAll());const e=y.matcher.exec(o);if(!e)break;const n=x(o.substring(S,e.index),e);S=e.index+n}return x(o.substr(S)),O.closeAllNodes(),O.finalize(),N=O.toHTML(),{relevance:I,value:N,language:e,illegal:!1,emitter:O,top:y}}catch(n){if(n.message&&n.message.includes("Illegal"))return{illegal:!0,illegalBy:{msg:n.message,context:o.slice(S-100,S+100),mode:n.mode},sofar:N,relevance:0,value:R(o),emitter:O};if(l)return{illegal:!1,relevance:0,value:R(o),emitter:O,language:e,top:y,errorRaised:n};throw n}}function v(e,n){n=n||f.languages||Object.keys(i);var t=function(e){const n={relevance:0,emitter:new f.__emitter(f),value:R(e),illegal:!1,top:h};return n.emitter.addText(e),n}(e),r=t;return n.filter(T).filter(I).forEach((function(n){var a=m(n,e,!1);a.language=n,a.relevance>r.relevance&&(r=a),a.relevance>t.relevance&&(r=t,t=a)})),r.language&&(t.second_best=r),t}function x(e){return f.tabReplace||f.useBR?e.replace(c,e=>"\n"===e?f.useBR?"
":e:f.tabReplace?e.replace(/\t/g,f.tabReplace):e):e}function E(e){let n=null;const t=function(e){var n=e.className+" ";n+=e.parentNode?e.parentNode.className:"";const t=f.languageDetectRe.exec(n);if(t){var r=T(t[1]);return r||(console.warn(g.replace("{}",t[1])),console.warn("Falling back to no-highlight mode for this block.",e)),r?t[1]:"no-highlight"}return n.split(/\s+/).find(e=>p(e)||T(e))}(e);if(p(t))return;S("before:highlightBlock",{block:e,language:t}),f.useBR?(n=document.createElement("div")).innerHTML=e.innerHTML.replace(/\n/g,"").replace(//g,"\n"):n=e;const r=n.textContent,a=t?b(t,r,!0):v(r),i=k(n);if(i.length){const e=document.createElement("div");e.innerHTML=a.value,a.value=O(i,k(e),r)}a.value=x(a.value),S("after:highlightBlock",{block:e,result:a}),e.innerHTML=a.value,e.className=function(e,n,t){var r=n?s[n]:t,a=[e.trim()];return e.match(/\bhljs\b/)||a.push("hljs"),e.includes(r)||a.push(r),a.join(" ").trim()}(e.className,t,a.language),e.result={language:a.language,re:a.relevance,relavance:a.relevance},a.second_best&&(e.second_best={language:a.second_best.language,re:a.second_best.relevance,relavance:a.second_best.relevance})}const N=()=>{if(!N.called){N.called=!0;var e=document.querySelectorAll("pre code");a.forEach.call(e,E)}};function T(e){return e=(e||"").toLowerCase(),i[e]||i[s[e]]}function A(e,{languageName:n}){"string"==typeof e&&(e=[e]),e.forEach(e=>{s[e]=n})}function I(e){var n=T(e);return n&&!n.disableAutodetect}function S(e,n){var t=e;o.forEach((function(e){e[t]&&e[t](n)}))}Object.assign(t,{highlight:b,highlightAuto:v,fixMarkup:x,highlightBlock:E,configure:function(e){f=y(f,e)},initHighlighting:N,initHighlightingOnLoad:function(){window.addEventListener("DOMContentLoaded",N,!1)},registerLanguage:function(e,n){var r=null;try{r=n(t)}catch(n){if(console.error("Language definition for '{}' could not be registered.".replace("{}",e)),!l)throw n;console.error(n),r=h}r.name||(r.name=e),i[e]=r,r.rawDefinition=n.bind(null,t),r.aliases&&A(r.aliases,{languageName:e})},listLanguages:function(){return Object.keys(i)},getLanguage:T,registerAliases:A,requireLanguage:function(e){var n=T(e);if(n)return n;throw Error("The '{}' language is required, but not loaded.".replace("{}",e))},autoDetection:I,inherit:y,addPlugin:function(e){o.push(e)}}),t.debugMode=function(){l=!1},t.safeMode=function(){l=!0},t.versionString="10.1.1";for(const n in _)"object"==typeof _[n]&&e(_[n]);return Object.assign(t,_),t}({})}();"object"==typeof exports&&"undefined"!=typeof module&&(module.exports=hljs);hljs.registerLanguage("php",function(){"use strict";return function(e){var r={begin:"\\$+[a-zA-Z_-ÿ][a-zA-Z0-9_-ÿ]*"},t={className:"meta",variants:[{begin:/<\?php/,relevance:10},{begin:/<\?[=]?/},{begin:/\?>/}]},a={className:"string",contains:[e.BACKSLASH_ESCAPE,t],variants:[{begin:'b"',end:'"'},{begin:"b'",end:"'"},e.inherit(e.APOS_STRING_MODE,{illegal:null}),e.inherit(e.QUOTE_STRING_MODE,{illegal:null})]},n={variants:[e.BINARY_NUMBER_MODE,e.C_NUMBER_MODE]},i={keyword:"__CLASS__ __DIR__ __FILE__ __FUNCTION__ __LINE__ __METHOD__ __NAMESPACE__ __TRAIT__ die echo exit include include_once print require require_once array abstract and as binary bool boolean break callable case catch class clone const continue declare default do double else elseif empty enddeclare endfor endforeach endif endswitch endwhile eval extends final finally float for foreach from global goto if implements instanceof insteadof int integer interface isset iterable list new object or private protected public real return string switch throw trait try unset use var void while xor yield",literal:"false null true",built_in:"Error|0 AppendIterator ArgumentCountError ArithmeticError ArrayIterator ArrayObject AssertionError BadFunctionCallException BadMethodCallException CachingIterator CallbackFilterIterator CompileError Countable DirectoryIterator DivisionByZeroError DomainException EmptyIterator ErrorException Exception FilesystemIterator FilterIterator GlobIterator InfiniteIterator InvalidArgumentException IteratorIterator LengthException LimitIterator LogicException MultipleIterator NoRewindIterator OutOfBoundsException OutOfRangeException OuterIterator OverflowException ParentIterator ParseError RangeException RecursiveArrayIterator RecursiveCachingIterator RecursiveCallbackFilterIterator RecursiveDirectoryIterator RecursiveFilterIterator RecursiveIterator RecursiveIteratorIterator RecursiveRegexIterator RecursiveTreeIterator RegexIterator RuntimeException SeekableIterator SplDoublyLinkedList SplFileInfo SplFileObject SplFixedArray SplHeap SplMaxHeap SplMinHeap SplObjectStorage SplObserver SplObserver SplPriorityQueue SplQueue SplStack SplSubject SplSubject SplTempFileObject TypeError UnderflowException UnexpectedValueException ArrayAccess Closure Generator Iterator IteratorAggregate Serializable Throwable Traversable WeakReference Directory __PHP_Incomplete_Class parent php_user_filter self static stdClass"};return{aliases:["php","php3","php4","php5","php6","php7"],case_insensitive:!0,keywords:i,contains:[e.HASH_COMMENT_MODE,e.COMMENT("//","$",{contains:[t]}),e.COMMENT("/\\*","\\*/",{contains:[{className:"doctag",begin:"@[A-Za-z]+"}]}),e.COMMENT("__halt_compiler.+?;",!1,{endsWithParent:!0,keywords:"__halt_compiler"}),{className:"string",begin:/<<<['"]?\w+['"]?$/,end:/^\w+;?$/,contains:[e.BACKSLASH_ESCAPE,{className:"subst",variants:[{begin:/\$\w+/},{begin:/\{\$/,end:/\}/}]}]},t,{className:"keyword",begin:/\$this\b/},r,{begin:/(::|->)+[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/},{className:"function",beginKeywords:"fn function",end:/[;{]/,excludeEnd:!0,illegal:"[$%\\[]",contains:[e.UNDERSCORE_TITLE_MODE,{className:"params",begin:"\\(",end:"\\)",excludeBegin:!0,excludeEnd:!0,keywords:i,contains:["self",r,e.C_BLOCK_COMMENT_MODE,a,n]}]},{className:"class",beginKeywords:"class interface",end:"{",excludeEnd:!0,illegal:/[:\(\$"]/,contains:[{beginKeywords:"extends implements"},e.UNDERSCORE_TITLE_MODE]},{beginKeywords:"namespace",end:";",illegal:/[\.']/,contains:[e.UNDERSCORE_TITLE_MODE]},{beginKeywords:"use",end:";",contains:[e.UNDERSCORE_TITLE_MODE]},{begin:"=>"},a,n]}}}());hljs.registerLanguage("nginx",function(){"use strict";return function(e){var n={className:"variable",variants:[{begin:/\$\d+/},{begin:/\$\{/,end:/}/},{begin:"[\\$\\@]"+e.UNDERSCORE_IDENT_RE}]},a={endsWithParent:!0,keywords:{$pattern:"[a-z/_]+",literal:"on off yes no true false none blocked debug info notice warn error crit select break last permanent redirect kqueue rtsig epoll poll /dev/poll"},relevance:0,illegal:"=>",contains:[e.HASH_COMMENT_MODE,{className:"string",contains:[e.BACKSLASH_ESCAPE,n],variants:[{begin:/"/,end:/"/},{begin:/'/,end:/'/}]},{begin:"([a-z]+):/",end:"\\s",endsWithParent:!0,excludeEnd:!0,contains:[n]},{className:"regexp",contains:[e.BACKSLASH_ESCAPE,n],variants:[{begin:"\\s\\^",end:"\\s|{|;",returnEnd:!0},{begin:"~\\*?\\s+",end:"\\s|{|;",returnEnd:!0},{begin:"\\*(\\.[a-z\\-]+)+"},{begin:"([a-z\\-]+\\.)+\\*"}]},{className:"number",begin:"\\b\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}(:\\d{1,5})?\\b"},{className:"number",begin:"\\b\\d+[kKmMgGdshdwy]*\\b",relevance:0},n]};return{name:"Nginx config",aliases:["nginxconf"],contains:[e.HASH_COMMENT_MODE,{begin:e.UNDERSCORE_IDENT_RE+"\\s+{",returnBegin:!0,end:"{",contains:[{className:"section",begin:e.UNDERSCORE_IDENT_RE}],relevance:0},{begin:e.UNDERSCORE_IDENT_RE+"\\s",end:";|{",returnBegin:!0,contains:[{className:"attribute",begin:e.UNDERSCORE_IDENT_RE,starts:a}],relevance:0}],illegal:"[^\\s\\}]"}}}());hljs.registerLanguage("csharp",function(){"use strict";return function(e){var n={keyword:"abstract as base bool break byte case catch char checked const continue decimal default delegate do double enum event explicit extern finally fixed float for foreach goto if implicit in int interface internal is lock long object operator out override params private protected public readonly ref sbyte sealed short sizeof stackalloc static string struct switch this try typeof uint ulong unchecked unsafe ushort using virtual void volatile while add alias ascending async await by descending dynamic equals from get global group into join let nameof on orderby partial remove select set value var when where yield",literal:"null false true"},i=e.inherit(e.TITLE_MODE,{begin:"[a-zA-Z](\\.?\\w)*"}),a={className:"number",variants:[{begin:"\\b(0b[01']+)"},{begin:"(-?)\\b([\\d']+(\\.[\\d']*)?|\\.[\\d']+)(u|U|l|L|ul|UL|f|F|b|B)"},{begin:"(-?)(\\b0[xX][a-fA-F0-9']+|(\\b[\\d']+(\\.[\\d']*)?|\\.[\\d']+)([eE][-+]?[\\d']+)?)"}],relevance:0},s={className:"string",begin:'@"',end:'"',contains:[{begin:'""'}]},t=e.inherit(s,{illegal:/\n/}),l={className:"subst",begin:"{",end:"}",keywords:n},r=e.inherit(l,{illegal:/\n/}),c={className:"string",begin:/\$"/,end:'"',illegal:/\n/,contains:[{begin:"{{"},{begin:"}}"},e.BACKSLASH_ESCAPE,r]},o={className:"string",begin:/\$@"/,end:'"',contains:[{begin:"{{"},{begin:"}}"},{begin:'""'},l]},g=e.inherit(o,{illegal:/\n/,contains:[{begin:"{{"},{begin:"}}"},{begin:'""'},r]});l.contains=[o,c,s,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,a,e.C_BLOCK_COMMENT_MODE],r.contains=[g,c,t,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,a,e.inherit(e.C_BLOCK_COMMENT_MODE,{illegal:/\n/})];var d={variants:[o,c,s,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE]},E={begin:"<",end:">",contains:[{beginKeywords:"in out"},i]},_=e.IDENT_RE+"(<"+e.IDENT_RE+"(\\s*,\\s*"+e.IDENT_RE+")*>)?(\\[\\])?",b={begin:"@"+e.IDENT_RE,relevance:0};return{name:"C#",aliases:["cs","c#"],keywords:n,illegal:/::/,contains:[e.COMMENT("///","$",{returnBegin:!0,contains:[{className:"doctag",variants:[{begin:"///",relevance:0},{begin:"\x3c!--|--\x3e"},{begin:""}]}]}),e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,{className:"meta",begin:"#",end:"$",keywords:{"meta-keyword":"if else elif endif define undef warning error line region endregion pragma checksum"}},d,a,{beginKeywords:"class interface",end:/[{;=]/,illegal:/[^\s:,]/,contains:[{beginKeywords:"where class"},i,E,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},{beginKeywords:"namespace",end:/[{;=]/,illegal:/[^\s:]/,contains:[i,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},{className:"meta",begin:"^\\s*\\[",excludeBegin:!0,end:"\\]",excludeEnd:!0,contains:[{className:"meta-string",begin:/"/,end:/"/}]},{beginKeywords:"new return throw await else",relevance:0},{className:"function",begin:"("+_+"\\s+)+"+e.IDENT_RE+"\\s*(\\<.+\\>)?\\s*\\(",returnBegin:!0,end:/\s*[{;=]/,excludeEnd:!0,keywords:n,contains:[{begin:e.IDENT_RE+"\\s*(\\<.+\\>)?\\s*\\(",returnBegin:!0,contains:[e.TITLE_MODE,E],relevance:0},{className:"params",begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:n,relevance:0,contains:[d,a,e.C_BLOCK_COMMENT_MODE]},e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},b]}}}());hljs.registerLanguage("perl",function(){"use strict";return function(e){var n={$pattern:/[\w.]+/,keyword:"getpwent getservent quotemeta msgrcv scalar kill dbmclose undef lc ma syswrite tr send umask sysopen shmwrite vec qx utime local oct semctl localtime readpipe do return format read sprintf dbmopen pop getpgrp not getpwnam rewinddir qq fileno qw endprotoent wait sethostent bless s|0 opendir continue each sleep endgrent shutdown dump chomp connect getsockname die socketpair close flock exists index shmget sub for endpwent redo lstat msgctl setpgrp abs exit select print ref gethostbyaddr unshift fcntl syscall goto getnetbyaddr join gmtime symlink semget splice x|0 getpeername recv log setsockopt cos last reverse gethostbyname getgrnam study formline endhostent times chop length gethostent getnetent pack getprotoent getservbyname rand mkdir pos chmod y|0 substr endnetent printf next open msgsnd readdir use unlink getsockopt getpriority rindex wantarray hex system getservbyport endservent int chr untie rmdir prototype tell listen fork shmread ucfirst setprotoent else sysseek link getgrgid shmctl waitpid unpack getnetbyname reset chdir grep split require caller lcfirst until warn while values shift telldir getpwuid my getprotobynumber delete and sort uc defined srand accept package seekdir getprotobyname semop our rename seek if q|0 chroot sysread setpwent no crypt getc chown sqrt write setnetent setpriority foreach tie sin msgget map stat getlogin unless elsif truncate exec keys glob tied closedir ioctl socket readlink eval xor readline binmode setservent eof ord bind alarm pipe atan2 getgrent exp time push setgrent gt lt or ne m|0 break given say state when"},t={className:"subst",begin:"[$@]\\{",end:"\\}",keywords:n},s={begin:"->{",end:"}"},r={variants:[{begin:/\$\d/},{begin:/[\$%@](\^\w\b|#\w+(::\w+)*|{\w+}|\w+(::\w*)*)/},{begin:/[\$%@][^\s\w{]/,relevance:0}]},i=[e.BACKSLASH_ESCAPE,t,r],a=[r,e.HASH_COMMENT_MODE,e.COMMENT("^\\=\\w","\\=cut",{endsWithParent:!0}),s,{className:"string",contains:i,variants:[{begin:"q[qwxr]?\\s*\\(",end:"\\)",relevance:5},{begin:"q[qwxr]?\\s*\\[",end:"\\]",relevance:5},{begin:"q[qwxr]?\\s*\\{",end:"\\}",relevance:5},{begin:"q[qwxr]?\\s*\\|",end:"\\|",relevance:5},{begin:"q[qwxr]?\\s*\\<",end:"\\>",relevance:5},{begin:"qw\\s+q",end:"q",relevance:5},{begin:"'",end:"'",contains:[e.BACKSLASH_ESCAPE]},{begin:'"',end:'"'},{begin:"`",end:"`",contains:[e.BACKSLASH_ESCAPE]},{begin:"{\\w+}",contains:[],relevance:0},{begin:"-?\\w+\\s*\\=\\>",contains:[],relevance:0}]},{className:"number",begin:"(\\b0[0-7_]+)|(\\b0x[0-9a-fA-F_]+)|(\\b[1-9][0-9_]*(\\.[0-9_]+)?)|[0_]\\b",relevance:0},{begin:"(\\/\\/|"+e.RE_STARTERS_RE+"|\\b(split|return|print|reverse|grep)\\b)\\s*",keywords:"split return print reverse grep",relevance:0,contains:[e.HASH_COMMENT_MODE,{className:"regexp",begin:"(s|tr|y)/(\\\\.|[^/])*/(\\\\.|[^/])*/[a-z]*",relevance:10},{className:"regexp",begin:"(m|qr)?/",end:"/[a-z]*",contains:[e.BACKSLASH_ESCAPE],relevance:0}]},{className:"function",beginKeywords:"sub",end:"(\\s*\\(.*?\\))?[;{]",excludeEnd:!0,relevance:5,contains:[e.TITLE_MODE]},{begin:"-\\w\\b",relevance:0},{begin:"^__DATA__$",end:"^__END__$",subLanguage:"mojolicious",contains:[{begin:"^@@.*",end:"$",className:"comment"}]}];return t.contains=a,s.contains=a,{name:"Perl",aliases:["pl","pm"],keywords:n,contains:a}}}());hljs.registerLanguage("swift",function(){"use strict";return function(e){var i={keyword:"#available #colorLiteral #column #else #elseif #endif #file #fileLiteral #function #if #imageLiteral #line #selector #sourceLocation _ __COLUMN__ __FILE__ __FUNCTION__ __LINE__ Any as as! as? associatedtype associativity break case catch class continue convenience default defer deinit didSet do dynamic dynamicType else enum extension fallthrough false fileprivate final for func get guard if import in indirect infix init inout internal is lazy left let mutating nil none nonmutating open operator optional override postfix precedence prefix private protocol Protocol public repeat required rethrows return right self Self set static struct subscript super switch throw throws true try try! try? Type typealias unowned var weak where while willSet",literal:"true false nil",built_in:"abs advance alignof alignofValue anyGenerator assert assertionFailure bridgeFromObjectiveC bridgeFromObjectiveCUnconditional bridgeToObjectiveC bridgeToObjectiveCUnconditional c compactMap contains count countElements countLeadingZeros debugPrint debugPrintln distance dropFirst dropLast dump encodeBitsAsWords enumerate equal fatalError filter find getBridgedObjectiveCType getVaList indices insertionSort isBridgedToObjectiveC isBridgedVerbatimToObjectiveC isUniquelyReferenced isUniquelyReferencedNonObjC join lazy lexicographicalCompare map max maxElement min minElement numericCast overlaps partition posix precondition preconditionFailure print println quickSort readLine reduce reflect reinterpretCast reverse roundUpToAlignment sizeof sizeofValue sort split startsWith stride strideof strideofValue swap toString transcode underestimateCount unsafeAddressOf unsafeBitCast unsafeDowncast unsafeUnwrap unsafeReflect withExtendedLifetime withObjectAtPlusZero withUnsafePointer withUnsafePointerToObject withUnsafeMutablePointer withUnsafeMutablePointers withUnsafePointer withUnsafePointers withVaList zip"},n=e.COMMENT("/\\*","\\*/",{contains:["self"]}),t={className:"subst",begin:/\\\(/,end:"\\)",keywords:i,contains:[]},a={className:"string",contains:[e.BACKSLASH_ESCAPE,t],variants:[{begin:/"""/,end:/"""/},{begin:/"/,end:/"/}]},r={className:"number",begin:"\\b([\\d_]+(\\.[\\deE_]+)?|0x[a-fA-F0-9_]+(\\.[a-fA-F0-9p_]+)?|0b[01_]+|0o[0-7_]+)\\b",relevance:0};return t.contains=[r],{name:"Swift",keywords:i,contains:[a,e.C_LINE_COMMENT_MODE,n,{className:"type",begin:"\\b[A-Z][\\wÀ-ʸ']*[!?]"},{className:"type",begin:"\\b[A-Z][\\wÀ-ʸ']*",relevance:0},r,{className:"function",beginKeywords:"func",end:"{",excludeEnd:!0,contains:[e.inherit(e.TITLE_MODE,{begin:/[A-Za-z$_][0-9A-Za-z$_]*/}),{begin://},{className:"params",begin:/\(/,end:/\)/,endsParent:!0,keywords:i,contains:["self",r,a,e.C_BLOCK_COMMENT_MODE,{begin:":"}],illegal:/["']/}],illegal:/\[|%/},{className:"class",beginKeywords:"struct protocol class extension enum",keywords:i,end:"\\{",excludeEnd:!0,contains:[e.inherit(e.TITLE_MODE,{begin:/[A-Za-z$_][\u00C0-\u02B80-9A-Za-z$_]*/})]},{className:"meta",begin:"(@discardableResult|@warn_unused_result|@exported|@lazy|@noescape|@NSCopying|@NSManaged|@objc|@objcMembers|@convention|@required|@noreturn|@IBAction|@IBDesignable|@IBInspectable|@IBOutlet|@infix|@prefix|@postfix|@autoclosure|@testable|@available|@nonobjc|@NSApplicationMain|@UIApplicationMain|@dynamicMemberLookup|@propertyWrapper)\\b"},{beginKeywords:"import",end:/$/,contains:[e.C_LINE_COMMENT_MODE,n]}]}}}());hljs.registerLanguage("makefile",function(){"use strict";return function(e){var i={className:"variable",variants:[{begin:"\\$\\("+e.UNDERSCORE_IDENT_RE+"\\)",contains:[e.BACKSLASH_ESCAPE]},{begin:/\$[@%`]+/}]}]}]};return{name:"HTML, XML",aliases:["html","xhtml","rss","atom","xjb","xsd","xsl","plist","wsf","svg"],case_insensitive:!0,contains:[{className:"meta",begin:"",relevance:10,contains:[a,i,t,s,{begin:"\\[",end:"\\]",contains:[{className:"meta",begin:"",contains:[a,s,i,t]}]}]},e.COMMENT("\x3c!--","--\x3e",{relevance:10}),{begin:"<\\!\\[CDATA\\[",end:"\\]\\]>",relevance:10},n,{className:"meta",begin:/<\?xml/,end:/\?>/,relevance:10},{className:"tag",begin:")",end:">",keywords:{name:"style"},contains:[c],starts:{end:"",returnEnd:!0,subLanguage:["css","xml"]}},{className:"tag",begin:")",end:">",keywords:{name:"script"},contains:[c],starts:{end:"<\/script>",returnEnd:!0,subLanguage:["javascript","handlebars","xml"]}},{className:"tag",begin:"",contains:[{className:"name",begin:/[^\/><\s]+/,relevance:0},c]}]}}}());hljs.registerLanguage("bash",function(){"use strict";return function(e){const s={};Object.assign(s,{className:"variable",variants:[{begin:/\$[\w\d#@][\w\d_]*/},{begin:/\$\{/,end:/\}/,contains:[{begin:/:-/,contains:[s]}]}]});const t={className:"subst",begin:/\$\(/,end:/\)/,contains:[e.BACKSLASH_ESCAPE]},n={className:"string",begin:/"/,end:/"/,contains:[e.BACKSLASH_ESCAPE,s,t]};t.contains.push(n);const a={begin:/\$\(\(/,end:/\)\)/,contains:[{begin:/\d+#[0-9a-f]+/,className:"number"},e.NUMBER_MODE,s]},i=e.SHEBANG({binary:"(fish|bash|zsh|sh|csh|ksh|tcsh|dash|scsh)",relevance:10}),c={className:"function",begin:/\w[\w\d_]*\s*\(\s*\)\s*\{/,returnBegin:!0,contains:[e.inherit(e.TITLE_MODE,{begin:/\w[\w\d_]*/})],relevance:0};return{name:"Bash",aliases:["sh","zsh"],keywords:{$pattern:/\b-?[a-z\._]+\b/,keyword:"if then else elif fi for while in do done case esac function",literal:"true false",built_in:"break cd continue eval exec exit export getopts hash pwd readonly return shift test times trap umask unset alias bind builtin caller command declare echo enable help let local logout mapfile printf read readarray source type typeset ulimit unalias set shopt autoload bg bindkey bye cap chdir clone comparguments compcall compctl compdescribe compfiles compgroups compquote comptags comptry compvalues dirs disable disown echotc echoti emulate fc fg float functions getcap getln history integer jobs kill limit log noglob popd print pushd pushln rehash sched setcap setopt stat suspend ttyctl unfunction unhash unlimit unsetopt vared wait whence where which zcompile zformat zftp zle zmodload zparseopts zprof zpty zregexparse zsocket zstyle ztcp",_:"-ne -eq -lt -gt -f -d -e -s -l -a"},contains:[i,e.SHEBANG(),c,a,e.HASH_COMMENT_MODE,n,{className:"",begin:/\\"/},{className:"string",begin:/'/,end:/'/},s]}}}());hljs.registerLanguage("c-like",function(){"use strict";return function(e){function t(e){return"(?:"+e+")?"}var n="(decltype\\(auto\\)|"+t("[a-zA-Z_]\\w*::")+"[a-zA-Z_]\\w*"+t("<.*?>")+")",r={className:"keyword",begin:"\\b[a-z\\d_]*_t\\b"},a={className:"string",variants:[{begin:'(u8?|U|L)?"',end:'"',illegal:"\\n",contains:[e.BACKSLASH_ESCAPE]},{begin:"(u8?|U|L)?'(\\\\(x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4,8}|[0-7]{3}|\\S)|.)",end:"'",illegal:"."},e.END_SAME_AS_BEGIN({begin:/(?:u8?|U|L)?R"([^()\\ ]{0,16})\(/,end:/\)([^()\\ ]{0,16})"/})]},i={className:"number",variants:[{begin:"\\b(0b[01']+)"},{begin:"(-?)\\b([\\d']+(\\.[\\d']*)?|\\.[\\d']+)(u|U|l|L|ul|UL|f|F|b|B)"},{begin:"(-?)(\\b0[xX][a-fA-F0-9']+|(\\b[\\d']+(\\.[\\d']*)?|\\.[\\d']+)([eE][-+]?[\\d']+)?)"}],relevance:0},s={className:"meta",begin:/#\s*[a-z]+\b/,end:/$/,keywords:{"meta-keyword":"if else elif endif define undef warning error line pragma _Pragma ifdef ifndef include"},contains:[{begin:/\\\n/,relevance:0},e.inherit(a,{className:"meta-string"}),{className:"meta-string",begin:/<.*?>/,end:/$/,illegal:"\\n"},e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},o={className:"title",begin:t("[a-zA-Z_]\\w*::")+e.IDENT_RE,relevance:0},c=t("[a-zA-Z_]\\w*::")+e.IDENT_RE+"\\s*\\(",l={keyword:"int float while private char char8_t char16_t char32_t catch import module export virtual operator sizeof dynamic_cast|10 typedef const_cast|10 const for static_cast|10 union namespace unsigned long volatile static protected bool template mutable if public friend do goto auto void enum else break extern using asm case typeid wchar_t short reinterpret_cast|10 default double register explicit signed typename try this switch continue inline delete alignas alignof constexpr consteval constinit decltype concept co_await co_return co_yield requires noexcept static_assert thread_local restrict final override atomic_bool atomic_char atomic_schar atomic_uchar atomic_short atomic_ushort atomic_int atomic_uint atomic_long atomic_ulong atomic_llong atomic_ullong new throw return and and_eq bitand bitor compl not not_eq or or_eq xor xor_eq",built_in:"std string wstring cin cout cerr clog stdin stdout stderr stringstream istringstream ostringstream auto_ptr deque list queue stack vector map set pair bitset multiset multimap unordered_set unordered_map unordered_multiset unordered_multimap priority_queue make_pair array shared_ptr abort terminate abs acos asin atan2 atan calloc ceil cosh cos exit exp fabs floor fmod fprintf fputs free frexp fscanf future isalnum isalpha iscntrl isdigit isgraph islower isprint ispunct isspace isupper isxdigit tolower toupper labs ldexp log10 log malloc realloc memchr memcmp memcpy memset modf pow printf putchar puts scanf sinh sin snprintf sprintf sqrt sscanf strcat strchr strcmp strcpy strcspn strlen strncat strncmp strncpy strpbrk strrchr strspn strstr tanh tan vfprintf vprintf vsprintf endl initializer_list unique_ptr _Bool complex _Complex imaginary _Imaginary",literal:"true false nullptr NULL"},d=[r,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,i,a],_={variants:[{begin:/=/,end:/;/},{begin:/\(/,end:/\)/},{beginKeywords:"new throw return else",end:/;/}],keywords:l,contains:d.concat([{begin:/\(/,end:/\)/,keywords:l,contains:d.concat(["self"]),relevance:0}]),relevance:0},u={className:"function",begin:"("+n+"[\\*&\\s]+)+"+c,returnBegin:!0,end:/[{;=]/,excludeEnd:!0,keywords:l,illegal:/[^\w\s\*&:<>]/,contains:[{begin:"decltype\\(auto\\)",keywords:l,relevance:0},{begin:c,returnBegin:!0,contains:[o],relevance:0},{className:"params",begin:/\(/,end:/\)/,keywords:l,relevance:0,contains:[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,a,i,r,{begin:/\(/,end:/\)/,keywords:l,relevance:0,contains:["self",e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,a,i,r]}]},r,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,s]};return{aliases:["c","cc","h","c++","h++","hpp","hh","hxx","cxx"],keywords:l,disableAutodetect:!0,illegal:"",keywords:l,contains:["self",r]},{begin:e.IDENT_RE+"::",keywords:l},{className:"class",beginKeywords:"class struct",end:/[{;:]/,contains:[{begin://,contains:["self"]},e.TITLE_MODE]}]),exports:{preprocessor:s,strings:a,keywords:l}}}}());hljs.registerLanguage("coffeescript",function(){"use strict";const e=["as","in","of","if","for","while","finally","var","new","function","do","return","void","else","break","catch","instanceof","with","throw","case","default","try","switch","continue","typeof","delete","let","yield","const","class","debugger","async","await","static","import","from","export","extends"],n=["true","false","null","undefined","NaN","Infinity"],a=[].concat(["setInterval","setTimeout","clearInterval","clearTimeout","require","exports","eval","isFinite","isNaN","parseFloat","parseInt","decodeURI","decodeURIComponent","encodeURI","encodeURIComponent","escape","unescape"],["arguments","this","super","console","window","document","localStorage","module","global"],["Intl","DataView","Number","Math","Date","String","RegExp","Object","Function","Boolean","Error","Symbol","Set","Map","WeakSet","WeakMap","Proxy","Reflect","JSON","Promise","Float64Array","Int16Array","Int32Array","Int8Array","Uint16Array","Uint32Array","Float32Array","Array","Uint8Array","Uint8ClampedArray","ArrayBuffer"],["EvalError","InternalError","RangeError","ReferenceError","SyntaxError","TypeError","URIError"]);return function(r){var t={keyword:e.concat(["then","unless","until","loop","by","when","and","or","is","isnt","not"]).filter((e=>n=>!e.includes(n))(["var","const","let","function","static"])).join(" "),literal:n.concat(["yes","no","on","off"]).join(" "),built_in:a.concat(["npm","print"]).join(" ")},i="[A-Za-z$_][0-9A-Za-z$_]*",s={className:"subst",begin:/#\{/,end:/}/,keywords:t},o=[r.BINARY_NUMBER_MODE,r.inherit(r.C_NUMBER_MODE,{starts:{end:"(\\s*/)?",relevance:0}}),{className:"string",variants:[{begin:/'''/,end:/'''/,contains:[r.BACKSLASH_ESCAPE]},{begin:/'/,end:/'/,contains:[r.BACKSLASH_ESCAPE]},{begin:/"""/,end:/"""/,contains:[r.BACKSLASH_ESCAPE,s]},{begin:/"/,end:/"/,contains:[r.BACKSLASH_ESCAPE,s]}]},{className:"regexp",variants:[{begin:"///",end:"///",contains:[s,r.HASH_COMMENT_MODE]},{begin:"//[gim]{0,3}(?=\\W)",relevance:0},{begin:/\/(?![ *]).*?(?![\\]).\/[gim]{0,3}(?=\W)/}]},{begin:"@"+i},{subLanguage:"javascript",excludeBegin:!0,excludeEnd:!0,variants:[{begin:"```",end:"```"},{begin:"`",end:"`"}]}];s.contains=o;var c=r.inherit(r.TITLE_MODE,{begin:i}),l={className:"params",begin:"\\([^\\(]",returnBegin:!0,contains:[{begin:/\(/,end:/\)/,keywords:t,contains:["self"].concat(o)}]};return{name:"CoffeeScript",aliases:["coffee","cson","iced"],keywords:t,illegal:/\/\*/,contains:o.concat([r.COMMENT("###","###"),r.HASH_COMMENT_MODE,{className:"function",begin:"^\\s*"+i+"\\s*=\\s*(\\(.*\\))?\\s*\\B[-=]>",end:"[-=]>",returnBegin:!0,contains:[c,l]},{begin:/[:\(,=]\s*/,relevance:0,contains:[{className:"function",begin:"(\\(.*\\))?\\s*\\B[-=]>",end:"[-=]>",returnBegin:!0,contains:[l]}]},{className:"class",beginKeywords:"class",end:"$",illegal:/[:="\[\]]/,contains:[{beginKeywords:"extends",endsWithParent:!0,illegal:/[:="\[\]]/,contains:[c]},c]},{begin:i+":",end:":",returnBegin:!0,returnEnd:!0,relevance:0}])}}}());hljs.registerLanguage("ruby",function(){"use strict";return function(e){var n="[a-zA-Z_]\\w*[!?=]?|[-+~]\\@|<<|>>|=~|===?|<=>|[<>]=?|\\*\\*|[-/+%^&*~`|]|\\[\\]=?",a={keyword:"and then defined module in return redo if BEGIN retry end for self when next until do begin unless END rescue else break undef not super class case require yield alias while ensure elsif or include attr_reader attr_writer attr_accessor",literal:"true false nil"},s={className:"doctag",begin:"@[A-Za-z]+"},i={begin:"#<",end:">"},r=[e.COMMENT("#","$",{contains:[s]}),e.COMMENT("^\\=begin","^\\=end",{contains:[s],relevance:10}),e.COMMENT("^__END__","\\n$")],c={className:"subst",begin:"#\\{",end:"}",keywords:a},t={className:"string",contains:[e.BACKSLASH_ESCAPE,c],variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/},{begin:/`/,end:/`/},{begin:"%[qQwWx]?\\(",end:"\\)"},{begin:"%[qQwWx]?\\[",end:"\\]"},{begin:"%[qQwWx]?{",end:"}"},{begin:"%[qQwWx]?<",end:">"},{begin:"%[qQwWx]?/",end:"/"},{begin:"%[qQwWx]?%",end:"%"},{begin:"%[qQwWx]?-",end:"-"},{begin:"%[qQwWx]?\\|",end:"\\|"},{begin:/\B\?(\\\d{1,3}|\\x[A-Fa-f0-9]{1,2}|\\u[A-Fa-f0-9]{4}|\\?\S)\b/},{begin:/<<[-~]?'?(\w+)(?:.|\n)*?\n\s*\1\b/,returnBegin:!0,contains:[{begin:/<<[-~]?'?/},e.END_SAME_AS_BEGIN({begin:/(\w+)/,end:/(\w+)/,contains:[e.BACKSLASH_ESCAPE,c]})]}]},b={className:"params",begin:"\\(",end:"\\)",endsParent:!0,keywords:a},d=[t,i,{className:"class",beginKeywords:"class module",end:"$|;",illegal:/=/,contains:[e.inherit(e.TITLE_MODE,{begin:"[A-Za-z_]\\w*(::\\w+)*(\\?|\\!)?"}),{begin:"<\\s*",contains:[{begin:"("+e.IDENT_RE+"::)?"+e.IDENT_RE}]}].concat(r)},{className:"function",beginKeywords:"def",end:"$|;",contains:[e.inherit(e.TITLE_MODE,{begin:n}),b].concat(r)},{begin:e.IDENT_RE+"::"},{className:"symbol",begin:e.UNDERSCORE_IDENT_RE+"(\\!|\\?)?:",relevance:0},{className:"symbol",begin:":(?!\\s)",contains:[t,{begin:n}],relevance:0},{className:"number",begin:"(\\b0[0-7_]+)|(\\b0x[0-9a-fA-F_]+)|(\\b[1-9][0-9_]*(\\.[0-9_]+)?)|[0_]\\b",relevance:0},{begin:"(\\$\\W)|((\\$|\\@\\@?)(\\w+))"},{className:"params",begin:/\|/,end:/\|/,keywords:a},{begin:"("+e.RE_STARTERS_RE+"|unless)\\s*",keywords:"unless",contains:[i,{className:"regexp",contains:[e.BACKSLASH_ESCAPE,c],illegal:/\n/,variants:[{begin:"/",end:"/[a-z]*"},{begin:"%r{",end:"}[a-z]*"},{begin:"%r\\(",end:"\\)[a-z]*"},{begin:"%r!",end:"![a-z]*"},{begin:"%r\\[",end:"\\][a-z]*"}]}].concat(r),relevance:0}].concat(r);c.contains=d,b.contains=d;var g=[{begin:/^\s*=>/,starts:{end:"$",contains:d}},{className:"meta",begin:"^([>?]>|[\\w#]+\\(\\w+\\):\\d+:\\d+>|(\\w+-)?\\d+\\.\\d+\\.\\d(p\\d+)?[^>]+>)",starts:{end:"$",contains:d}}];return{name:"Ruby",aliases:["rb","gemspec","podspec","thor","irb"],keywords:a,illegal:/\/\*/,contains:r.concat(g).concat(d)}}}());hljs.registerLanguage("yaml",function(){"use strict";return function(e){var n="true false yes no null",a="[\\w#;/?:@&=+$,.~*\\'()[\\]]+",s={className:"string",relevance:0,variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/},{begin:/\S+/}],contains:[e.BACKSLASH_ESCAPE,{className:"template-variable",variants:[{begin:"{{",end:"}}"},{begin:"%{",end:"}"}]}]},i=e.inherit(s,{variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/},{begin:/[^\s,{}[\]]+/}]}),l={end:",",endsWithParent:!0,excludeEnd:!0,contains:[],keywords:n,relevance:0},t={begin:"{",end:"}",contains:[l],illegal:"\\n",relevance:0},g={begin:"\\[",end:"\\]",contains:[l],illegal:"\\n",relevance:0},b=[{className:"attr",variants:[{begin:"\\w[\\w :\\/.-]*:(?=[ \t]|$)"},{begin:'"\\w[\\w :\\/.-]*":(?=[ \t]|$)'},{begin:"'\\w[\\w :\\/.-]*':(?=[ \t]|$)"}]},{className:"meta",begin:"^---s*$",relevance:10},{className:"string",begin:"[\\|>]([0-9]?[+-])?[ ]*\\n( *)[\\S ]+\\n(\\2[\\S ]+\\n?)*"},{begin:"<%[%=-]?",end:"[%-]?%>",subLanguage:"ruby",excludeBegin:!0,excludeEnd:!0,relevance:0},{className:"type",begin:"!\\w+!"+a},{className:"type",begin:"!<"+a+">"},{className:"type",begin:"!"+a},{className:"type",begin:"!!"+a},{className:"meta",begin:"&"+e.UNDERSCORE_IDENT_RE+"$"},{className:"meta",begin:"\\*"+e.UNDERSCORE_IDENT_RE+"$"},{className:"bullet",begin:"\\-(?=[ ]|$)",relevance:0},e.HASH_COMMENT_MODE,{beginKeywords:n,keywords:{literal:n}},{className:"number",begin:"\\b[0-9]{4}(-[0-9][0-9]){0,2}([Tt \\t][0-9][0-9]?(:[0-9][0-9]){2})?(\\.[0-9]*)?([ \\t])*(Z|[-+][0-9][0-9]?(:[0-9][0-9])?)?\\b"},{className:"number",begin:e.C_NUMBER_RE+"\\b"},t,g,s],c=[...b];return c.pop(),c.push(i),l.contains=c,{name:"YAML",case_insensitive:!0,aliases:["yml","YAML"],contains:b}}}());hljs.registerLanguage("d",function(){"use strict";return function(e){var a={$pattern:e.UNDERSCORE_IDENT_RE,keyword:"abstract alias align asm assert auto body break byte case cast catch class const continue debug default delete deprecated do else enum export extern final finally for foreach foreach_reverse|10 goto if immutable import in inout int interface invariant is lazy macro mixin module new nothrow out override package pragma private protected public pure ref return scope shared static struct super switch synchronized template this throw try typedef typeid typeof union unittest version void volatile while with __FILE__ __LINE__ __gshared|10 __thread __traits __DATE__ __EOF__ __TIME__ __TIMESTAMP__ __VENDOR__ __VERSION__",built_in:"bool cdouble cent cfloat char creal dchar delegate double dstring float function idouble ifloat ireal long real short string ubyte ucent uint ulong ushort wchar wstring",literal:"false null true"},d="((0|[1-9][\\d_]*)|0[bB][01_]+|0[xX]([\\da-fA-F][\\da-fA-F_]*|_[\\da-fA-F][\\da-fA-F_]*))",n="\\\\(['\"\\?\\\\abfnrtv]|u[\\dA-Fa-f]{4}|[0-7]{1,3}|x[\\dA-Fa-f]{2}|U[\\dA-Fa-f]{8})|&[a-zA-Z\\d]{2,};",t={className:"number",begin:"\\b"+d+"(L|u|U|Lu|LU|uL|UL)?",relevance:0},_={className:"number",begin:"\\b(((0[xX](([\\da-fA-F][\\da-fA-F_]*|_[\\da-fA-F][\\da-fA-F_]*)\\.([\\da-fA-F][\\da-fA-F_]*|_[\\da-fA-F][\\da-fA-F_]*)|\\.?([\\da-fA-F][\\da-fA-F_]*|_[\\da-fA-F][\\da-fA-F_]*))[pP][+-]?(0|[1-9][\\d_]*|\\d[\\d_]*|[\\d_]+?\\d))|((0|[1-9][\\d_]*|\\d[\\d_]*|[\\d_]+?\\d)(\\.\\d*|([eE][+-]?(0|[1-9][\\d_]*|\\d[\\d_]*|[\\d_]+?\\d)))|\\d+\\.(0|[1-9][\\d_]*|\\d[\\d_]*|[\\d_]+?\\d)(0|[1-9][\\d_]*|\\d[\\d_]*|[\\d_]+?\\d)|\\.(0|[1-9][\\d_]*)([eE][+-]?(0|[1-9][\\d_]*|\\d[\\d_]*|[\\d_]+?\\d))?))([fF]|L|i|[fF]i|Li)?|"+d+"(i|[fF]i|Li))",relevance:0},r={className:"string",begin:"'("+n+"|.)",end:"'",illegal:"."},i={className:"string",begin:'"',contains:[{begin:n,relevance:0}],end:'"[cwd]?'},s=e.COMMENT("\\/\\+","\\+\\/",{contains:["self"],relevance:10});return{name:"D",keywords:a,contains:[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,s,{className:"string",begin:'x"[\\da-fA-F\\s\\n\\r]*"[cwd]?',relevance:10},i,{className:"string",begin:'[rq]"',end:'"[cwd]?',relevance:5},{className:"string",begin:"`",end:"`[cwd]?"},{className:"string",begin:'q"\\{',end:'\\}"'},_,t,r,{className:"meta",begin:"^#!",end:"$",relevance:5},{className:"meta",begin:"#(line)",end:"$",relevance:5},{className:"keyword",begin:"@[a-zA-Z_][a-zA-Z_\\d]*"}]}}}());hljs.registerLanguage("properties",function(){"use strict";return function(e){var n="[ \\t\\f]*",t="("+n+"[:=]"+n+"|[ \\t\\f]+)",a="([^\\\\:= \\t\\f\\n]|\\\\.)+",s={end:t,relevance:0,starts:{className:"string",end:/$/,relevance:0,contains:[{begin:"\\\\\\n"}]}};return{name:".properties",case_insensitive:!0,illegal:/\S/,contains:[e.COMMENT("^\\s*[!#]","$"),{begin:"([^\\\\\\W:= \\t\\f\\n]|\\\\.)+"+t,returnBegin:!0,contains:[{className:"attr",begin:"([^\\\\\\W:= \\t\\f\\n]|\\\\.)+",endsParent:!0,relevance:0}],starts:s},{begin:a+t,returnBegin:!0,relevance:0,contains:[{className:"meta",begin:a,endsParent:!0,relevance:0}],starts:s},{className:"attr",relevance:0,begin:a+n+"$"}]}}}());hljs.registerLanguage("http",function(){"use strict";return function(e){var n="HTTP/[0-9\\.]+";return{name:"HTTP",aliases:["https"],illegal:"\\S",contains:[{begin:"^"+n,end:"$",contains:[{className:"number",begin:"\\b\\d{3}\\b"}]},{begin:"^[A-Z]+ (.*?) "+n+"$",returnBegin:!0,end:"$",contains:[{className:"string",begin:" ",end:" ",excludeBegin:!0,excludeEnd:!0},{begin:n},{className:"keyword",begin:"[A-Z]+"}]},{className:"attribute",begin:"^\\w",end:": ",excludeEnd:!0,illegal:"\\n|\\s|=",starts:{end:"$",relevance:0}},{begin:"\\n\\n",starts:{subLanguage:[],endsWithParent:!0}}]}}}());hljs.registerLanguage("haskell",function(){"use strict";return function(e){var n={variants:[e.COMMENT("--","$"),e.COMMENT("{-","-}",{contains:["self"]})]},i={className:"meta",begin:"{-#",end:"#-}"},a={className:"meta",begin:"^#",end:"$"},s={className:"type",begin:"\\b[A-Z][\\w']*",relevance:0},l={begin:"\\(",end:"\\)",illegal:'"',contains:[i,a,{className:"type",begin:"\\b[A-Z][\\w]*(\\((\\.\\.|,|\\w+)\\))?"},e.inherit(e.TITLE_MODE,{begin:"[_a-z][\\w']*"}),n]};return{name:"Haskell",aliases:["hs"],keywords:"let in if then else case of where do module import hiding qualified type data newtype deriving class instance as default infix infixl infixr foreign export ccall stdcall cplusplus jvm dotnet safe unsafe family forall mdo proc rec",contains:[{beginKeywords:"module",end:"where",keywords:"module where",contains:[l,n],illegal:"\\W\\.|;"},{begin:"\\bimport\\b",end:"$",keywords:"import qualified as hiding",contains:[l,n],illegal:"\\W\\.|;"},{className:"class",begin:"^(\\s*)?(class|instance)\\b",end:"where",keywords:"class family instance where",contains:[s,l,n]},{className:"class",begin:"\\b(data|(new)?type)\\b",end:"$",keywords:"data family type newtype deriving",contains:[i,s,l,{begin:"{",end:"}",contains:l.contains},n]},{beginKeywords:"default",end:"$",contains:[s,l,n]},{beginKeywords:"infix infixl infixr",end:"$",contains:[e.C_NUMBER_MODE,n]},{begin:"\\bforeign\\b",end:"$",keywords:"foreign import export ccall stdcall cplusplus jvm dotnet safe unsafe",contains:[s,e.QUOTE_STRING_MODE,n]},{className:"meta",begin:"#!\\/usr\\/bin\\/env runhaskell",end:"$"},i,a,e.QUOTE_STRING_MODE,e.C_NUMBER_MODE,s,e.inherit(e.TITLE_MODE,{begin:"^[_a-z][\\w']*"}),n,{begin:"->|<-"}]}}}());hljs.registerLanguage("handlebars",function(){"use strict";function e(...e){return e.map(e=>(function(e){return e?"string"==typeof e?e:e.source:null})(e)).join("")}return function(n){const a={"builtin-name":"action bindattr collection component concat debugger each each-in get hash if in input link-to loc log lookup mut outlet partial query-params render template textarea unbound unless view with yield"},t=/\[.*?\]/,s=/[^\s!"#%&'()*+,.\/;<=>@\[\\\]^`{|}~]+/,i=e("(",/'.*?'/,"|",/".*?"/,"|",t,"|",s,"|",/\.|\//,")+"),r=e("(",t,"|",s,")(?==)"),l={begin:i,lexemes:/[\w.\/]+/},c=n.inherit(l,{keywords:{literal:"true false undefined null"}}),o={begin:/\(/,end:/\)/},m={className:"attr",begin:r,relevance:0,starts:{begin:/=/,end:/=/,starts:{contains:[n.NUMBER_MODE,n.QUOTE_STRING_MODE,n.APOS_STRING_MODE,c,o]}}},d={contains:[n.NUMBER_MODE,n.QUOTE_STRING_MODE,n.APOS_STRING_MODE,{begin:/as\s+\|/,keywords:{keyword:"as"},end:/\|/,contains:[{begin:/\w+/}]},m,c,o],returnEnd:!0},g=n.inherit(l,{className:"name",keywords:a,starts:n.inherit(d,{end:/\)/})});o.contains=[g];const u=n.inherit(l,{keywords:a,className:"name",starts:n.inherit(d,{end:/}}/})}),b=n.inherit(l,{keywords:a,className:"name"}),h=n.inherit(l,{className:"name",keywords:a,starts:n.inherit(d,{end:/}}/})});return{name:"Handlebars",aliases:["hbs","html.hbs","html.handlebars","htmlbars"],case_insensitive:!0,subLanguage:"xml",contains:[{begin:/\\\{\{/,skip:!0},{begin:/\\\\(?=\{\{)/,skip:!0},n.COMMENT(/\{\{!--/,/--\}\}/),n.COMMENT(/\{\{!/,/\}\}/),{className:"template-tag",begin:/\{\{\{\{(?!\/)/,end:/\}\}\}\}/,contains:[u],starts:{end:/\{\{\{\{\//,returnEnd:!0,subLanguage:"xml"}},{className:"template-tag",begin:/\{\{\{\{\//,end:/\}\}\}\}/,contains:[b]},{className:"template-tag",begin:/\{\{#/,end:/\}\}/,contains:[u]},{className:"template-tag",begin:/\{\{(?=else\}\})/,end:/\}\}/,keywords:"else"},{className:"template-tag",begin:/\{\{\//,end:/\}\}/,contains:[b]},{className:"template-variable",begin:/\{\{\{/,end:/\}\}\}/,contains:[h]},{className:"template-variable",begin:/\{\{/,end:/\}\}/,contains:[h]}]}}}());hljs.registerLanguage("rust",function(){"use strict";return function(e){var n="([ui](8|16|32|64|128|size)|f(32|64))?",t="drop i8 i16 i32 i64 i128 isize u8 u16 u32 u64 u128 usize f32 f64 str char bool Box Option Result String Vec Copy Send Sized Sync Drop Fn FnMut FnOnce ToOwned Clone Debug PartialEq PartialOrd Eq Ord AsRef AsMut Into From Default Iterator Extend IntoIterator DoubleEndedIterator ExactSizeIterator SliceConcatExt ToString assert! assert_eq! bitflags! bytes! cfg! col! concat! concat_idents! debug_assert! debug_assert_eq! env! panic! file! format! format_args! include_bin! include_str! line! local_data_key! module_path! option_env! print! println! select! stringify! try! unimplemented! unreachable! vec! write! writeln! macro_rules! assert_ne! debug_assert_ne!";return{name:"Rust",aliases:["rs"],keywords:{$pattern:e.IDENT_RE+"!?",keyword:"abstract as async await become box break const continue crate do dyn else enum extern false final fn for if impl in let loop macro match mod move mut override priv pub ref return self Self static struct super trait true try type typeof unsafe unsized use virtual where while yield",literal:"true false Some None Ok Err",built_in:t},illegal:""}]}}}());hljs.registerLanguage("cpp",function(){"use strict";return function(e){var t=e.getLanguage("c-like").rawDefinition();return t.disableAutodetect=!1,t.name="C++",t.aliases=["cc","c++","h++","hpp","hh","hxx","cxx"],t}}());hljs.registerLanguage("ini",function(){"use strict";function e(e){return e?"string"==typeof e?e:e.source:null}function n(...n){return n.map(n=>e(n)).join("")}return function(a){var s={className:"number",relevance:0,variants:[{begin:/([\+\-]+)?[\d]+_[\d_]+/},{begin:a.NUMBER_RE}]},i=a.COMMENT();i.variants=[{begin:/;/,end:/$/},{begin:/#/,end:/$/}];var t={className:"variable",variants:[{begin:/\$[\w\d"][\w\d_]*/},{begin:/\$\{(.*?)}/}]},r={className:"literal",begin:/\bon|off|true|false|yes|no\b/},l={className:"string",contains:[a.BACKSLASH_ESCAPE],variants:[{begin:"'''",end:"'''",relevance:10},{begin:'"""',end:'"""',relevance:10},{begin:'"',end:'"'},{begin:"'",end:"'"}]},c={begin:/\[/,end:/\]/,contains:[i,r,t,l,s,"self"],relevance:0},g="("+[/[A-Za-z0-9_-]+/,/"(\\"|[^"])*"/,/'[^']*'/].map(n=>e(n)).join("|")+")";return{name:"TOML, also INI",aliases:["toml"],case_insensitive:!0,illegal:/\S/,contains:[i,{className:"section",begin:/\[+/,end:/\]+/},{begin:n(g,"(\\s*\\.\\s*",g,")*",n("(?=",/\s*=\s*[^#\s]/,")")),className:"attr",starts:{end:/$/,contains:[i,c,r,t,l,s]}}]}}}());hljs.registerLanguage("objectivec",function(){"use strict";return function(e){var n=/[a-zA-Z@][a-zA-Z0-9_]*/,_={$pattern:n,keyword:"@interface @class @protocol @implementation"};return{name:"Objective-C",aliases:["mm","objc","obj-c"],keywords:{$pattern:n,keyword:"int float while char export sizeof typedef const struct for union unsigned long volatile static bool mutable if do return goto void enum else break extern asm case short default double register explicit signed typename this switch continue wchar_t inline readonly assign readwrite self @synchronized id typeof nonatomic super unichar IBOutlet IBAction strong weak copy in out inout bycopy byref oneway __strong __weak __block __autoreleasing @private @protected @public @try @property @end @throw @catch @finally @autoreleasepool @synthesize @dynamic @selector @optional @required @encode @package @import @defs @compatibility_alias __bridge __bridge_transfer __bridge_retained __bridge_retain __covariant __contravariant __kindof _Nonnull _Nullable _Null_unspecified __FUNCTION__ __PRETTY_FUNCTION__ __attribute__ getter setter retain unsafe_unretained nonnull nullable null_unspecified null_resettable class instancetype NS_DESIGNATED_INITIALIZER NS_UNAVAILABLE NS_REQUIRES_SUPER NS_RETURNS_INNER_POINTER NS_INLINE NS_AVAILABLE NS_DEPRECATED NS_ENUM NS_OPTIONS NS_SWIFT_UNAVAILABLE NS_ASSUME_NONNULL_BEGIN NS_ASSUME_NONNULL_END NS_REFINED_FOR_SWIFT NS_SWIFT_NAME NS_SWIFT_NOTHROW NS_DURING NS_HANDLER NS_ENDHANDLER NS_VALUERETURN NS_VOIDRETURN",literal:"false true FALSE TRUE nil YES NO NULL",built_in:"BOOL dispatch_once_t dispatch_queue_t dispatch_sync dispatch_async dispatch_once"},illegal:"/,end:/$/,illegal:"\\n"},e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},{className:"class",begin:"("+_.keyword.split(" ").join("|")+")\\b",end:"({|$)",excludeEnd:!0,keywords:_,contains:[e.UNDERSCORE_TITLE_MODE]},{begin:"\\."+e.UNDERSCORE_IDENT_RE,relevance:0}]}}}());hljs.registerLanguage("apache",function(){"use strict";return function(e){var n={className:"number",begin:"\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}(:\\d{1,5})?"};return{name:"Apache config",aliases:["apacheconf"],case_insensitive:!0,contains:[e.HASH_COMMENT_MODE,{className:"section",begin:"",contains:[n,{className:"number",begin:":\\d{1,5}"},e.inherit(e.QUOTE_STRING_MODE,{relevance:0})]},{className:"attribute",begin:/\w+/,relevance:0,keywords:{nomarkup:"order deny allow setenv rewriterule rewriteengine rewritecond documentroot sethandler errordocument loadmodule options header listen serverroot servername"},starts:{end:/$/,relevance:0,keywords:{literal:"on off all deny allow"},contains:[{className:"meta",begin:"\\s\\[",end:"\\]$"},{className:"variable",begin:"[\\$%]\\{",end:"\\}",contains:["self",{className:"number",begin:"[\\$%]\\d+"}]},n,{className:"number",begin:"\\d+"},e.QUOTE_STRING_MODE]}}],illegal:/\S/}}}());hljs.registerLanguage("java",function(){"use strict";function e(e){return e?"string"==typeof e?e:e.source:null}function n(e){return a("(",e,")?")}function a(...n){return n.map(n=>e(n)).join("")}function s(...n){return"("+n.map(n=>e(n)).join("|")+")"}return function(e){var t="false synchronized int abstract float private char boolean var static null if const for true while long strictfp finally protected import native final void enum else break transient catch instanceof byte super volatile case assert short package default double public try this switch continue throws protected public private module requires exports do",i={className:"meta",begin:"@[À-ʸa-zA-Z_$][À-ʸa-zA-Z_$0-9]*",contains:[{begin:/\(/,end:/\)/,contains:["self"]}]},r=e=>a("[",e,"]+([",e,"_]*[",e,"]+)?"),c={className:"number",variants:[{begin:`\\b(0[bB]${r("01")})[lL]?`},{begin:`\\b(0${r("0-7")})[dDfFlL]?`},{begin:a(/\b0[xX]/,s(a(r("a-fA-F0-9"),/\./,r("a-fA-F0-9")),a(r("a-fA-F0-9"),/\.?/),a(/\./,r("a-fA-F0-9"))),/([pP][+-]?(\d+))?/,/[fFdDlL]?/)},{begin:a(/\b/,s(a(/\d*\./,r("\\d")),r("\\d")),/[eE][+-]?[\d]+[dDfF]?/)},{begin:a(/\b/,r(/\d/),n(/\.?/),n(r(/\d/)),/[dDfFlL]?/)}],relevance:0};return{name:"Java",aliases:["jsp"],keywords:t,illegal:/<\/|#/,contains:[e.COMMENT("/\\*\\*","\\*/",{relevance:0,contains:[{begin:/\w+@/,relevance:0},{className:"doctag",begin:"@[A-Za-z]+"}]}),e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,{className:"class",beginKeywords:"class interface",end:/[{;=]/,excludeEnd:!0,keywords:"class interface",illegal:/[:"\[\]]/,contains:[{beginKeywords:"extends implements"},e.UNDERSCORE_TITLE_MODE]},{beginKeywords:"new throw return else",relevance:0},{className:"function",begin:"([À-ʸa-zA-Z_$][À-ʸa-zA-Z_$0-9]*(<[À-ʸa-zA-Z_$][À-ʸa-zA-Z_$0-9]*(\\s*,\\s*[À-ʸa-zA-Z_$][À-ʸa-zA-Z_$0-9]*)*>)?\\s+)+"+e.UNDERSCORE_IDENT_RE+"\\s*\\(",returnBegin:!0,end:/[{;=]/,excludeEnd:!0,keywords:t,contains:[{begin:e.UNDERSCORE_IDENT_RE+"\\s*\\(",returnBegin:!0,relevance:0,contains:[e.UNDERSCORE_TITLE_MODE]},{className:"params",begin:/\(/,end:/\)/,keywords:t,relevance:0,contains:[i,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,e.C_NUMBER_MODE,e.C_BLOCK_COMMENT_MODE]},e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},c,i]}}}());hljs.registerLanguage("x86asm",function(){"use strict";return function(s){return{name:"Intel x86 Assembly",case_insensitive:!0,keywords:{$pattern:"[.%]?"+s.IDENT_RE,keyword:"lock rep repe repz repne repnz xaquire xrelease bnd nobnd aaa aad aam aas adc add and arpl bb0_reset bb1_reset bound bsf bsr bswap bt btc btr bts call cbw cdq cdqe clc cld cli clts cmc cmp cmpsb cmpsd cmpsq cmpsw cmpxchg cmpxchg486 cmpxchg8b cmpxchg16b cpuid cpu_read cpu_write cqo cwd cwde daa das dec div dmint emms enter equ f2xm1 fabs fadd faddp fbld fbstp fchs fclex fcmovb fcmovbe fcmove fcmovnb fcmovnbe fcmovne fcmovnu fcmovu fcom fcomi fcomip fcomp fcompp fcos fdecstp fdisi fdiv fdivp fdivr fdivrp femms feni ffree ffreep fiadd ficom ficomp fidiv fidivr fild fimul fincstp finit fist fistp fisttp fisub fisubr fld fld1 fldcw fldenv fldl2e fldl2t fldlg2 fldln2 fldpi fldz fmul fmulp fnclex fndisi fneni fninit fnop fnsave fnstcw fnstenv fnstsw fpatan fprem fprem1 fptan frndint frstor fsave fscale fsetpm fsin fsincos fsqrt fst fstcw fstenv fstp fstsw fsub fsubp fsubr fsubrp ftst fucom fucomi fucomip fucomp fucompp fxam fxch fxtract fyl2x fyl2xp1 hlt ibts icebp idiv imul in inc incbin insb insd insw int int01 int1 int03 int3 into invd invpcid invlpg invlpga iret iretd iretq iretw jcxz jecxz jrcxz jmp jmpe lahf lar lds lea leave les lfence lfs lgdt lgs lidt lldt lmsw loadall loadall286 lodsb lodsd lodsq lodsw loop loope loopne loopnz loopz lsl lss ltr mfence monitor mov movd movq movsb movsd movsq movsw movsx movsxd movzx mul mwait neg nop not or out outsb outsd outsw packssdw packsswb packuswb paddb paddd paddsb paddsiw paddsw paddusb paddusw paddw pand pandn pause paveb pavgusb pcmpeqb pcmpeqd pcmpeqw pcmpgtb pcmpgtd pcmpgtw pdistib pf2id pfacc pfadd pfcmpeq pfcmpge pfcmpgt pfmax pfmin pfmul pfrcp pfrcpit1 pfrcpit2 pfrsqit1 pfrsqrt pfsub pfsubr pi2fd pmachriw pmaddwd pmagw pmulhriw pmulhrwa pmulhrwc pmulhw pmullw pmvgezb pmvlzb pmvnzb pmvzb pop popa popad popaw popf popfd popfq popfw por prefetch prefetchw pslld psllq psllw psrad psraw psrld psrlq psrlw psubb psubd psubsb psubsiw psubsw psubusb psubusw psubw punpckhbw punpckhdq punpckhwd punpcklbw punpckldq punpcklwd push pusha pushad pushaw pushf pushfd pushfq pushfw pxor rcl rcr rdshr rdmsr rdpmc rdtsc rdtscp ret retf retn rol ror rdm rsdc rsldt rsm rsts sahf sal salc sar sbb scasb scasd scasq scasw sfence sgdt shl shld shr shrd sidt sldt skinit smi smint smintold smsw stc std sti stosb stosd stosq stosw str sub svdc svldt svts swapgs syscall sysenter sysexit sysret test ud0 ud1 ud2b ud2 ud2a umov verr verw fwait wbinvd wrshr wrmsr xadd xbts xchg xlatb xlat xor cmove cmovz cmovne cmovnz cmova cmovnbe cmovae cmovnb cmovb cmovnae cmovbe cmovna cmovg cmovnle cmovge cmovnl cmovl cmovnge cmovle cmovng cmovc cmovnc cmovo cmovno cmovs cmovns cmovp cmovpe cmovnp cmovpo je jz jne jnz ja jnbe jae jnb jb jnae jbe jna jg jnle jge jnl jl jnge jle jng jc jnc jo jno js jns jpo jnp jpe jp sete setz setne setnz seta setnbe setae setnb setnc setb setnae setcset setbe setna setg setnle setge setnl setl setnge setle setng sets setns seto setno setpe setp setpo setnp addps addss andnps andps cmpeqps cmpeqss cmpleps cmpless cmpltps cmpltss cmpneqps cmpneqss cmpnleps cmpnless cmpnltps cmpnltss cmpordps cmpordss cmpunordps cmpunordss cmpps cmpss comiss cvtpi2ps cvtps2pi cvtsi2ss cvtss2si cvttps2pi cvttss2si divps divss ldmxcsr maxps maxss minps minss movaps movhps movlhps movlps movhlps movmskps movntps movss movups mulps mulss orps rcpps rcpss rsqrtps rsqrtss shufps sqrtps sqrtss stmxcsr subps subss ucomiss unpckhps unpcklps xorps fxrstor fxrstor64 fxsave fxsave64 xgetbv xsetbv xsave xsave64 xsaveopt xsaveopt64 xrstor xrstor64 prefetchnta prefetcht0 prefetcht1 prefetcht2 maskmovq movntq pavgb pavgw pextrw pinsrw pmaxsw pmaxub pminsw pminub pmovmskb pmulhuw psadbw pshufw pf2iw pfnacc pfpnacc pi2fw pswapd maskmovdqu clflush movntdq movnti movntpd movdqa movdqu movdq2q movq2dq paddq pmuludq pshufd pshufhw pshuflw pslldq psrldq psubq punpckhqdq punpcklqdq addpd addsd andnpd andpd cmpeqpd cmpeqsd cmplepd cmplesd cmpltpd cmpltsd cmpneqpd cmpneqsd cmpnlepd cmpnlesd cmpnltpd cmpnltsd cmpordpd cmpordsd cmpunordpd cmpunordsd cmppd comisd cvtdq2pd cvtdq2ps cvtpd2dq cvtpd2pi cvtpd2ps cvtpi2pd cvtps2dq cvtps2pd cvtsd2si cvtsd2ss cvtsi2sd cvtss2sd cvttpd2pi cvttpd2dq cvttps2dq cvttsd2si divpd divsd maxpd maxsd minpd minsd movapd movhpd movlpd movmskpd movupd mulpd mulsd orpd shufpd sqrtpd sqrtsd subpd subsd ucomisd unpckhpd unpcklpd xorpd addsubpd addsubps haddpd haddps hsubpd hsubps lddqu movddup movshdup movsldup clgi stgi vmcall vmclear vmfunc vmlaunch vmload vmmcall vmptrld vmptrst vmread vmresume vmrun vmsave vmwrite vmxoff vmxon invept invvpid pabsb pabsw pabsd palignr phaddw phaddd phaddsw phsubw phsubd phsubsw pmaddubsw pmulhrsw pshufb psignb psignw psignd extrq insertq movntsd movntss lzcnt blendpd blendps blendvpd blendvps dppd dpps extractps insertps movntdqa mpsadbw packusdw pblendvb pblendw pcmpeqq pextrb pextrd pextrq phminposuw pinsrb pinsrd pinsrq pmaxsb pmaxsd pmaxud pmaxuw pminsb pminsd pminud pminuw pmovsxbw pmovsxbd pmovsxbq pmovsxwd pmovsxwq pmovsxdq pmovzxbw pmovzxbd pmovzxbq pmovzxwd pmovzxwq pmovzxdq pmuldq pmulld ptest roundpd roundps roundsd roundss crc32 pcmpestri pcmpestrm pcmpistri pcmpistrm pcmpgtq popcnt getsec pfrcpv pfrsqrtv movbe aesenc aesenclast aesdec aesdeclast aesimc aeskeygenassist vaesenc vaesenclast vaesdec vaesdeclast vaesimc vaeskeygenassist vaddpd vaddps vaddsd vaddss vaddsubpd vaddsubps vandpd vandps vandnpd vandnps vblendpd vblendps vblendvpd vblendvps vbroadcastss vbroadcastsd vbroadcastf128 vcmpeq_ospd vcmpeqpd vcmplt_ospd vcmpltpd vcmple_ospd vcmplepd vcmpunord_qpd vcmpunordpd vcmpneq_uqpd vcmpneqpd vcmpnlt_uspd vcmpnltpd vcmpnle_uspd vcmpnlepd vcmpord_qpd vcmpordpd vcmpeq_uqpd vcmpnge_uspd vcmpngepd vcmpngt_uspd vcmpngtpd vcmpfalse_oqpd vcmpfalsepd vcmpneq_oqpd vcmpge_ospd vcmpgepd vcmpgt_ospd vcmpgtpd vcmptrue_uqpd vcmptruepd vcmplt_oqpd vcmple_oqpd vcmpunord_spd vcmpneq_uspd vcmpnlt_uqpd vcmpnle_uqpd vcmpord_spd vcmpeq_uspd vcmpnge_uqpd vcmpngt_uqpd vcmpfalse_ospd vcmpneq_ospd vcmpge_oqpd vcmpgt_oqpd vcmptrue_uspd vcmppd vcmpeq_osps vcmpeqps vcmplt_osps vcmpltps vcmple_osps vcmpleps vcmpunord_qps vcmpunordps vcmpneq_uqps vcmpneqps vcmpnlt_usps vcmpnltps vcmpnle_usps vcmpnleps vcmpord_qps vcmpordps vcmpeq_uqps vcmpnge_usps vcmpngeps vcmpngt_usps vcmpngtps vcmpfalse_oqps vcmpfalseps vcmpneq_oqps vcmpge_osps vcmpgeps vcmpgt_osps vcmpgtps vcmptrue_uqps vcmptrueps vcmplt_oqps vcmple_oqps vcmpunord_sps vcmpneq_usps vcmpnlt_uqps vcmpnle_uqps vcmpord_sps vcmpeq_usps vcmpnge_uqps vcmpngt_uqps vcmpfalse_osps vcmpneq_osps vcmpge_oqps vcmpgt_oqps vcmptrue_usps vcmpps vcmpeq_ossd vcmpeqsd vcmplt_ossd vcmpltsd vcmple_ossd vcmplesd vcmpunord_qsd vcmpunordsd vcmpneq_uqsd vcmpneqsd vcmpnlt_ussd vcmpnltsd vcmpnle_ussd vcmpnlesd vcmpord_qsd vcmpordsd vcmpeq_uqsd vcmpnge_ussd vcmpngesd vcmpngt_ussd vcmpngtsd vcmpfalse_oqsd vcmpfalsesd vcmpneq_oqsd vcmpge_ossd vcmpgesd vcmpgt_ossd vcmpgtsd vcmptrue_uqsd vcmptruesd vcmplt_oqsd vcmple_oqsd vcmpunord_ssd vcmpneq_ussd vcmpnlt_uqsd vcmpnle_uqsd vcmpord_ssd vcmpeq_ussd vcmpnge_uqsd vcmpngt_uqsd vcmpfalse_ossd vcmpneq_ossd vcmpge_oqsd vcmpgt_oqsd vcmptrue_ussd vcmpsd vcmpeq_osss vcmpeqss vcmplt_osss vcmpltss vcmple_osss vcmpless vcmpunord_qss vcmpunordss vcmpneq_uqss vcmpneqss vcmpnlt_usss vcmpnltss vcmpnle_usss vcmpnless vcmpord_qss vcmpordss vcmpeq_uqss vcmpnge_usss vcmpngess vcmpngt_usss vcmpngtss vcmpfalse_oqss vcmpfalsess vcmpneq_oqss vcmpge_osss vcmpgess vcmpgt_osss vcmpgtss vcmptrue_uqss vcmptruess vcmplt_oqss vcmple_oqss vcmpunord_sss vcmpneq_usss vcmpnlt_uqss vcmpnle_uqss vcmpord_sss vcmpeq_usss vcmpnge_uqss vcmpngt_uqss vcmpfalse_osss vcmpneq_osss vcmpge_oqss vcmpgt_oqss vcmptrue_usss vcmpss vcomisd vcomiss vcvtdq2pd vcvtdq2ps vcvtpd2dq vcvtpd2ps vcvtps2dq vcvtps2pd vcvtsd2si vcvtsd2ss vcvtsi2sd vcvtsi2ss vcvtss2sd vcvtss2si vcvttpd2dq vcvttps2dq vcvttsd2si vcvttss2si vdivpd vdivps vdivsd vdivss vdppd vdpps vextractf128 vextractps vhaddpd vhaddps vhsubpd vhsubps vinsertf128 vinsertps vlddqu vldqqu vldmxcsr vmaskmovdqu vmaskmovps vmaskmovpd vmaxpd vmaxps vmaxsd vmaxss vminpd vminps vminsd vminss vmovapd vmovaps vmovd vmovq vmovddup vmovdqa vmovqqa vmovdqu vmovqqu vmovhlps vmovhpd vmovhps vmovlhps vmovlpd vmovlps vmovmskpd vmovmskps vmovntdq vmovntqq vmovntdqa vmovntpd vmovntps vmovsd vmovshdup vmovsldup vmovss vmovupd vmovups vmpsadbw vmulpd vmulps vmulsd vmulss vorpd vorps vpabsb vpabsw vpabsd vpacksswb vpackssdw vpackuswb vpackusdw vpaddb vpaddw vpaddd vpaddq vpaddsb vpaddsw vpaddusb vpaddusw vpalignr vpand vpandn vpavgb vpavgw vpblendvb vpblendw vpcmpestri vpcmpestrm vpcmpistri vpcmpistrm vpcmpeqb vpcmpeqw vpcmpeqd vpcmpeqq vpcmpgtb vpcmpgtw vpcmpgtd vpcmpgtq vpermilpd vpermilps vperm2f128 vpextrb vpextrw vpextrd vpextrq vphaddw vphaddd vphaddsw vphminposuw vphsubw vphsubd vphsubsw vpinsrb vpinsrw vpinsrd vpinsrq vpmaddwd vpmaddubsw vpmaxsb vpmaxsw vpmaxsd vpmaxub vpmaxuw vpmaxud vpminsb vpminsw vpminsd vpminub vpminuw vpminud vpmovmskb vpmovsxbw vpmovsxbd vpmovsxbq vpmovsxwd vpmovsxwq vpmovsxdq vpmovzxbw vpmovzxbd vpmovzxbq vpmovzxwd vpmovzxwq vpmovzxdq vpmulhuw vpmulhrsw vpmulhw vpmullw vpmulld vpmuludq vpmuldq vpor vpsadbw vpshufb vpshufd vpshufhw vpshuflw vpsignb vpsignw vpsignd vpslldq vpsrldq vpsllw vpslld vpsllq vpsraw vpsrad vpsrlw vpsrld vpsrlq vptest vpsubb vpsubw vpsubd vpsubq vpsubsb vpsubsw vpsubusb vpsubusw vpunpckhbw vpunpckhwd vpunpckhdq vpunpckhqdq vpunpcklbw vpunpcklwd vpunpckldq vpunpcklqdq vpxor vrcpps vrcpss vrsqrtps vrsqrtss vroundpd vroundps vroundsd vroundss vshufpd vshufps vsqrtpd vsqrtps vsqrtsd vsqrtss vstmxcsr vsubpd vsubps vsubsd vsubss vtestps vtestpd vucomisd vucomiss vunpckhpd vunpckhps vunpcklpd vunpcklps vxorpd vxorps vzeroall vzeroupper pclmullqlqdq pclmulhqlqdq pclmullqhqdq pclmulhqhqdq pclmulqdq vpclmullqlqdq vpclmulhqlqdq vpclmullqhqdq vpclmulhqhqdq vpclmulqdq vfmadd132ps vfmadd132pd vfmadd312ps vfmadd312pd vfmadd213ps vfmadd213pd vfmadd123ps vfmadd123pd vfmadd231ps vfmadd231pd vfmadd321ps vfmadd321pd vfmaddsub132ps vfmaddsub132pd vfmaddsub312ps vfmaddsub312pd vfmaddsub213ps vfmaddsub213pd vfmaddsub123ps vfmaddsub123pd vfmaddsub231ps vfmaddsub231pd vfmaddsub321ps vfmaddsub321pd vfmsub132ps vfmsub132pd vfmsub312ps vfmsub312pd vfmsub213ps vfmsub213pd vfmsub123ps vfmsub123pd vfmsub231ps vfmsub231pd vfmsub321ps vfmsub321pd vfmsubadd132ps vfmsubadd132pd vfmsubadd312ps vfmsubadd312pd vfmsubadd213ps vfmsubadd213pd vfmsubadd123ps vfmsubadd123pd vfmsubadd231ps vfmsubadd231pd vfmsubadd321ps vfmsubadd321pd vfnmadd132ps vfnmadd132pd vfnmadd312ps vfnmadd312pd vfnmadd213ps vfnmadd213pd vfnmadd123ps vfnmadd123pd vfnmadd231ps vfnmadd231pd vfnmadd321ps vfnmadd321pd vfnmsub132ps vfnmsub132pd vfnmsub312ps vfnmsub312pd vfnmsub213ps vfnmsub213pd vfnmsub123ps vfnmsub123pd vfnmsub231ps vfnmsub231pd vfnmsub321ps vfnmsub321pd vfmadd132ss vfmadd132sd vfmadd312ss vfmadd312sd vfmadd213ss vfmadd213sd vfmadd123ss vfmadd123sd vfmadd231ss vfmadd231sd vfmadd321ss vfmadd321sd vfmsub132ss vfmsub132sd vfmsub312ss vfmsub312sd vfmsub213ss vfmsub213sd vfmsub123ss vfmsub123sd vfmsub231ss vfmsub231sd vfmsub321ss vfmsub321sd vfnmadd132ss vfnmadd132sd vfnmadd312ss vfnmadd312sd vfnmadd213ss vfnmadd213sd vfnmadd123ss vfnmadd123sd vfnmadd231ss vfnmadd231sd vfnmadd321ss vfnmadd321sd vfnmsub132ss vfnmsub132sd vfnmsub312ss vfnmsub312sd vfnmsub213ss vfnmsub213sd vfnmsub123ss vfnmsub123sd vfnmsub231ss vfnmsub231sd vfnmsub321ss vfnmsub321sd rdfsbase rdgsbase rdrand wrfsbase wrgsbase vcvtph2ps vcvtps2ph adcx adox rdseed clac stac xstore xcryptecb xcryptcbc xcryptctr xcryptcfb xcryptofb montmul xsha1 xsha256 llwpcb slwpcb lwpval lwpins vfmaddpd vfmaddps vfmaddsd vfmaddss vfmaddsubpd vfmaddsubps vfmsubaddpd vfmsubaddps vfmsubpd vfmsubps vfmsubsd vfmsubss vfnmaddpd vfnmaddps vfnmaddsd vfnmaddss vfnmsubpd vfnmsubps vfnmsubsd vfnmsubss vfrczpd vfrczps vfrczsd vfrczss vpcmov vpcomb vpcomd vpcomq vpcomub vpcomud vpcomuq vpcomuw vpcomw vphaddbd vphaddbq vphaddbw vphadddq vphaddubd vphaddubq vphaddubw vphaddudq vphadduwd vphadduwq vphaddwd vphaddwq vphsubbw vphsubdq vphsubwd vpmacsdd vpmacsdqh vpmacsdql vpmacssdd vpmacssdqh vpmacssdql vpmacsswd vpmacssww vpmacswd vpmacsww vpmadcsswd vpmadcswd vpperm vprotb vprotd vprotq vprotw vpshab vpshad vpshaq vpshaw vpshlb vpshld vpshlq vpshlw vbroadcasti128 vpblendd vpbroadcastb vpbroadcastw vpbroadcastd vpbroadcastq vpermd vpermpd vpermps vpermq vperm2i128 vextracti128 vinserti128 vpmaskmovd vpmaskmovq vpsllvd vpsllvq vpsravd vpsrlvd vpsrlvq vgatherdpd vgatherqpd vgatherdps vgatherqps vpgatherdd vpgatherqd vpgatherdq vpgatherqq xabort xbegin xend xtest andn bextr blci blcic blsi blsic blcfill blsfill blcmsk blsmsk blsr blcs bzhi mulx pdep pext rorx sarx shlx shrx tzcnt tzmsk t1mskc valignd valignq vblendmpd vblendmps vbroadcastf32x4 vbroadcastf64x4 vbroadcasti32x4 vbroadcasti64x4 vcompresspd vcompressps vcvtpd2udq vcvtps2udq vcvtsd2usi vcvtss2usi vcvttpd2udq vcvttps2udq vcvttsd2usi vcvttss2usi vcvtudq2pd vcvtudq2ps vcvtusi2sd vcvtusi2ss vexpandpd vexpandps vextractf32x4 vextractf64x4 vextracti32x4 vextracti64x4 vfixupimmpd vfixupimmps vfixupimmsd vfixupimmss vgetexppd vgetexpps vgetexpsd vgetexpss vgetmantpd vgetmantps vgetmantsd vgetmantss vinsertf32x4 vinsertf64x4 vinserti32x4 vinserti64x4 vmovdqa32 vmovdqa64 vmovdqu32 vmovdqu64 vpabsq vpandd vpandnd vpandnq vpandq vpblendmd vpblendmq vpcmpltd vpcmpled vpcmpneqd vpcmpnltd vpcmpnled vpcmpd vpcmpltq vpcmpleq vpcmpneqq vpcmpnltq vpcmpnleq vpcmpq vpcmpequd vpcmpltud vpcmpleud vpcmpnequd vpcmpnltud vpcmpnleud vpcmpud vpcmpequq vpcmpltuq vpcmpleuq vpcmpnequq vpcmpnltuq vpcmpnleuq vpcmpuq vpcompressd vpcompressq vpermi2d vpermi2pd vpermi2ps vpermi2q vpermt2d vpermt2pd vpermt2ps vpermt2q vpexpandd vpexpandq vpmaxsq vpmaxuq vpminsq vpminuq vpmovdb vpmovdw vpmovqb vpmovqd vpmovqw vpmovsdb vpmovsdw vpmovsqb vpmovsqd vpmovsqw vpmovusdb vpmovusdw vpmovusqb vpmovusqd vpmovusqw vpord vporq vprold vprolq vprolvd vprolvq vprord vprorq vprorvd vprorvq vpscatterdd vpscatterdq vpscatterqd vpscatterqq vpsraq vpsravq vpternlogd vpternlogq vptestmd vptestmq vptestnmd vptestnmq vpxord vpxorq vrcp14pd vrcp14ps vrcp14sd vrcp14ss vrndscalepd vrndscaleps vrndscalesd vrndscaless vrsqrt14pd vrsqrt14ps vrsqrt14sd vrsqrt14ss vscalefpd vscalefps vscalefsd vscalefss vscatterdpd vscatterdps vscatterqpd vscatterqps vshuff32x4 vshuff64x2 vshufi32x4 vshufi64x2 kandnw kandw kmovw knotw kortestw korw kshiftlw kshiftrw kunpckbw kxnorw kxorw vpbroadcastmb2q vpbroadcastmw2d vpconflictd vpconflictq vplzcntd vplzcntq vexp2pd vexp2ps vrcp28pd vrcp28ps vrcp28sd vrcp28ss vrsqrt28pd vrsqrt28ps vrsqrt28sd vrsqrt28ss vgatherpf0dpd vgatherpf0dps vgatherpf0qpd vgatherpf0qps vgatherpf1dpd vgatherpf1dps vgatherpf1qpd vgatherpf1qps vscatterpf0dpd vscatterpf0dps vscatterpf0qpd vscatterpf0qps vscatterpf1dpd vscatterpf1dps vscatterpf1qpd vscatterpf1qps prefetchwt1 bndmk bndcl bndcu bndcn bndmov bndldx bndstx sha1rnds4 sha1nexte sha1msg1 sha1msg2 sha256rnds2 sha256msg1 sha256msg2 hint_nop0 hint_nop1 hint_nop2 hint_nop3 hint_nop4 hint_nop5 hint_nop6 hint_nop7 hint_nop8 hint_nop9 hint_nop10 hint_nop11 hint_nop12 hint_nop13 hint_nop14 hint_nop15 hint_nop16 hint_nop17 hint_nop18 hint_nop19 hint_nop20 hint_nop21 hint_nop22 hint_nop23 hint_nop24 hint_nop25 hint_nop26 hint_nop27 hint_nop28 hint_nop29 hint_nop30 hint_nop31 hint_nop32 hint_nop33 hint_nop34 hint_nop35 hint_nop36 hint_nop37 hint_nop38 hint_nop39 hint_nop40 hint_nop41 hint_nop42 hint_nop43 hint_nop44 hint_nop45 hint_nop46 hint_nop47 hint_nop48 hint_nop49 hint_nop50 hint_nop51 hint_nop52 hint_nop53 hint_nop54 hint_nop55 hint_nop56 hint_nop57 hint_nop58 hint_nop59 hint_nop60 hint_nop61 hint_nop62 hint_nop63",built_in:"ip eip rip al ah bl bh cl ch dl dh sil dil bpl spl r8b r9b r10b r11b r12b r13b r14b r15b ax bx cx dx si di bp sp r8w r9w r10w r11w r12w r13w r14w r15w eax ebx ecx edx esi edi ebp esp eip r8d r9d r10d r11d r12d r13d r14d r15d rax rbx rcx rdx rsi rdi rbp rsp r8 r9 r10 r11 r12 r13 r14 r15 cs ds es fs gs ss st st0 st1 st2 st3 st4 st5 st6 st7 mm0 mm1 mm2 mm3 mm4 mm5 mm6 mm7 xmm0 xmm1 xmm2 xmm3 xmm4 xmm5 xmm6 xmm7 xmm8 xmm9 xmm10 xmm11 xmm12 xmm13 xmm14 xmm15 xmm16 xmm17 xmm18 xmm19 xmm20 xmm21 xmm22 xmm23 xmm24 xmm25 xmm26 xmm27 xmm28 xmm29 xmm30 xmm31 ymm0 ymm1 ymm2 ymm3 ymm4 ymm5 ymm6 ymm7 ymm8 ymm9 ymm10 ymm11 ymm12 ymm13 ymm14 ymm15 ymm16 ymm17 ymm18 ymm19 ymm20 ymm21 ymm22 ymm23 ymm24 ymm25 ymm26 ymm27 ymm28 ymm29 ymm30 ymm31 zmm0 zmm1 zmm2 zmm3 zmm4 zmm5 zmm6 zmm7 zmm8 zmm9 zmm10 zmm11 zmm12 zmm13 zmm14 zmm15 zmm16 zmm17 zmm18 zmm19 zmm20 zmm21 zmm22 zmm23 zmm24 zmm25 zmm26 zmm27 zmm28 zmm29 zmm30 zmm31 k0 k1 k2 k3 k4 k5 k6 k7 bnd0 bnd1 bnd2 bnd3 cr0 cr1 cr2 cr3 cr4 cr8 dr0 dr1 dr2 dr3 dr8 tr3 tr4 tr5 tr6 tr7 r0 r1 r2 r3 r4 r5 r6 r7 r0b r1b r2b r3b r4b r5b r6b r7b r0w r1w r2w r3w r4w r5w r6w r7w r0d r1d r2d r3d r4d r5d r6d r7d r0h r1h r2h r3h r0l r1l r2l r3l r4l r5l r6l r7l r8l r9l r10l r11l r12l r13l r14l r15l db dw dd dq dt ddq do dy dz resb resw resd resq rest resdq reso resy resz incbin equ times byte word dword qword nosplit rel abs seg wrt strict near far a32 ptr",meta:"%define %xdefine %+ %undef %defstr %deftok %assign %strcat %strlen %substr %rotate %elif %else %endif %if %ifmacro %ifctx %ifidn %ifidni %ifid %ifnum %ifstr %iftoken %ifempty %ifenv %error %warning %fatal %rep %endrep %include %push %pop %repl %pathsearch %depend %use %arg %stacksize %local %line %comment %endcomment .nolist __FILE__ __LINE__ __SECT__ __BITS__ __OUTPUT_FORMAT__ __DATE__ __TIME__ __DATE_NUM__ __TIME_NUM__ __UTC_DATE__ __UTC_TIME__ __UTC_DATE_NUM__ __UTC_TIME_NUM__ __PASS__ struc endstruc istruc at iend align alignb sectalign daz nodaz up down zero default option assume public bits use16 use32 use64 default section segment absolute extern global common cpu float __utf16__ __utf16le__ __utf16be__ __utf32__ __utf32le__ __utf32be__ __float8__ __float16__ __float32__ __float64__ __float80m__ __float80e__ __float128l__ __float128h__ __Infinity__ __QNaN__ __SNaN__ Inf NaN QNaN SNaN float8 float16 float32 float64 float80m float80e float128l float128h __FLOAT_DAZ__ __FLOAT_ROUND__ __FLOAT__"},contains:[s.COMMENT(";","$",{relevance:0}),{className:"number",variants:[{begin:"\\b(?:([0-9][0-9_]*)?\\.[0-9_]*(?:[eE][+-]?[0-9_]+)?|(0[Xx])?[0-9][0-9_]*\\.?[0-9_]*(?:[pP](?:[+-]?[0-9_]+)?)?)\\b",relevance:0},{begin:"\\$[0-9][0-9A-Fa-f]*",relevance:0},{begin:"\\b(?:[0-9A-Fa-f][0-9A-Fa-f_]*[Hh]|[0-9][0-9_]*[DdTt]?|[0-7][0-7_]*[QqOo]|[0-1][0-1_]*[BbYy])\\b"},{begin:"\\b(?:0[Xx][0-9A-Fa-f_]+|0[DdTt][0-9_]+|0[QqOo][0-7_]+|0[BbYy][0-1_]+)\\b"}]},s.QUOTE_STRING_MODE,{className:"string",variants:[{begin:"'",end:"[^\\\\]'"},{begin:"`",end:"[^\\\\]`"}],relevance:0},{className:"symbol",variants:[{begin:"^\\s*[A-Za-z._?][A-Za-z0-9_$#@~.?]*(:|\\s+label)"},{begin:"^\\s*%%[A-Za-z0-9_$#@~.?]*:"}],relevance:0},{className:"subst",begin:"%[0-9]+",relevance:0},{className:"subst",begin:"%!S+",relevance:0},{className:"meta",begin:/^\s*\.[\w_-]+/}]}}}());hljs.registerLanguage("kotlin",function(){"use strict";return function(e){var n={keyword:"abstract as val var vararg get set class object open private protected public noinline crossinline dynamic final enum if else do while for when throw try catch finally import package is in fun override companion reified inline lateinit init interface annotation data sealed internal infix operator out by constructor super tailrec where const inner suspend typealias external expect actual trait volatile transient native default",built_in:"Byte Short Char Int Long Boolean Float Double Void Unit Nothing",literal:"true false null"},a={className:"symbol",begin:e.UNDERSCORE_IDENT_RE+"@"},i={className:"subst",begin:"\\${",end:"}",contains:[e.C_NUMBER_MODE]},s={className:"variable",begin:"\\$"+e.UNDERSCORE_IDENT_RE},t={className:"string",variants:[{begin:'"""',end:'"""(?=[^"])',contains:[s,i]},{begin:"'",end:"'",illegal:/\n/,contains:[e.BACKSLASH_ESCAPE]},{begin:'"',end:'"',illegal:/\n/,contains:[e.BACKSLASH_ESCAPE,s,i]}]};i.contains.push(t);var r={className:"meta",begin:"@(?:file|property|field|get|set|receiver|param|setparam|delegate)\\s*:(?:\\s*"+e.UNDERSCORE_IDENT_RE+")?"},l={className:"meta",begin:"@"+e.UNDERSCORE_IDENT_RE,contains:[{begin:/\(/,end:/\)/,contains:[e.inherit(t,{className:"meta-string"})]}]},c=e.COMMENT("/\\*","\\*/",{contains:[e.C_BLOCK_COMMENT_MODE]}),o={variants:[{className:"type",begin:e.UNDERSCORE_IDENT_RE},{begin:/\(/,end:/\)/,contains:[]}]},d=o;return d.variants[1].contains=[o],o.variants[1].contains=[d],{name:"Kotlin",aliases:["kt"],keywords:n,contains:[e.COMMENT("/\\*\\*","\\*/",{relevance:0,contains:[{className:"doctag",begin:"@[A-Za-z]+"}]}),e.C_LINE_COMMENT_MODE,c,{className:"keyword",begin:/\b(break|continue|return|this)\b/,starts:{contains:[{className:"symbol",begin:/@\w+/}]}},a,r,l,{className:"function",beginKeywords:"fun",end:"[(]|$",returnBegin:!0,excludeEnd:!0,keywords:n,illegal:/fun\s+(<.*>)?[^\s\(]+(\s+[^\s\(]+)\s*=/,relevance:5,contains:[{begin:e.UNDERSCORE_IDENT_RE+"\\s*\\(",returnBegin:!0,relevance:0,contains:[e.UNDERSCORE_TITLE_MODE]},{className:"type",begin://,keywords:"reified",relevance:0},{className:"params",begin:/\(/,end:/\)/,endsParent:!0,keywords:n,relevance:0,contains:[{begin:/:/,end:/[=,\/]/,endsWithParent:!0,contains:[o,e.C_LINE_COMMENT_MODE,c],relevance:0},e.C_LINE_COMMENT_MODE,c,r,l,t,e.C_NUMBER_MODE]},c]},{className:"class",beginKeywords:"class interface trait",end:/[:\{(]|$/,excludeEnd:!0,illegal:"extends implements",contains:[{beginKeywords:"public protected internal private constructor"},e.UNDERSCORE_TITLE_MODE,{className:"type",begin://,excludeBegin:!0,excludeEnd:!0,relevance:0},{className:"type",begin:/[,:]\s*/,end:/[<\(,]|$/,excludeBegin:!0,returnEnd:!0},r,l]},t,{className:"meta",begin:"^#!/usr/bin/env",end:"$",illegal:"\n"},{className:"number",begin:"\\b(0[bB]([01]+[01_]+[01]+|[01]+)|0[xX]([a-fA-F0-9]+[a-fA-F0-9_]+[a-fA-F0-9]+|[a-fA-F0-9]+)|(([\\d]+[\\d_]+[\\d]+|[\\d]+)(\\.([\\d]+[\\d_]+[\\d]+|[\\d]+))?|\\.([\\d]+[\\d_]+[\\d]+|[\\d]+))([eE][-+]?\\d+)?)[lLfF]?",relevance:0}]}}}());hljs.registerLanguage("armasm",function(){"use strict";return function(s){const e={variants:[s.COMMENT("^[ \\t]*(?=#)","$",{relevance:0,excludeBegin:!0}),s.COMMENT("[;@]","$",{relevance:0}),s.C_LINE_COMMENT_MODE,s.C_BLOCK_COMMENT_MODE]};return{name:"ARM Assembly",case_insensitive:!0,aliases:["arm"],keywords:{$pattern:"\\.?"+s.IDENT_RE,meta:".2byte .4byte .align .ascii .asciz .balign .byte .code .data .else .end .endif .endm .endr .equ .err .exitm .extern .global .hword .if .ifdef .ifndef .include .irp .long .macro .rept .req .section .set .skip .space .text .word .arm .thumb .code16 .code32 .force_thumb .thumb_func .ltorg ALIAS ALIGN ARM AREA ASSERT ATTR CN CODE CODE16 CODE32 COMMON CP DATA DCB DCD DCDU DCDO DCFD DCFDU DCI DCQ DCQU DCW DCWU DN ELIF ELSE END ENDFUNC ENDIF ENDP ENTRY EQU EXPORT EXPORTAS EXTERN FIELD FILL FUNCTION GBLA GBLL GBLS GET GLOBAL IF IMPORT INCBIN INCLUDE INFO KEEP LCLA LCLL LCLS LTORG MACRO MAP MEND MEXIT NOFP OPT PRESERVE8 PROC QN READONLY RELOC REQUIRE REQUIRE8 RLIST FN ROUT SETA SETL SETS SN SPACE SUBT THUMB THUMBX TTL WHILE WEND ",built_in:"r0 r1 r2 r3 r4 r5 r6 r7 r8 r9 r10 r11 r12 r13 r14 r15 pc lr sp ip sl sb fp a1 a2 a3 a4 v1 v2 v3 v4 v5 v6 v7 v8 f0 f1 f2 f3 f4 f5 f6 f7 p0 p1 p2 p3 p4 p5 p6 p7 p8 p9 p10 p11 p12 p13 p14 p15 c0 c1 c2 c3 c4 c5 c6 c7 c8 c9 c10 c11 c12 c13 c14 c15 q0 q1 q2 q3 q4 q5 q6 q7 q8 q9 q10 q11 q12 q13 q14 q15 cpsr_c cpsr_x cpsr_s cpsr_f cpsr_cx cpsr_cxs cpsr_xs cpsr_xsf cpsr_sf cpsr_cxsf spsr_c spsr_x spsr_s spsr_f spsr_cx spsr_cxs spsr_xs spsr_xsf spsr_sf spsr_cxsf s0 s1 s2 s3 s4 s5 s6 s7 s8 s9 s10 s11 s12 s13 s14 s15 s16 s17 s18 s19 s20 s21 s22 s23 s24 s25 s26 s27 s28 s29 s30 s31 d0 d1 d2 d3 d4 d5 d6 d7 d8 d9 d10 d11 d12 d13 d14 d15 d16 d17 d18 d19 d20 d21 d22 d23 d24 d25 d26 d27 d28 d29 d30 d31 {PC} {VAR} {TRUE} {FALSE} {OPT} {CONFIG} {ENDIAN} {CODESIZE} {CPU} {FPU} {ARCHITECTURE} {PCSTOREOFFSET} {ARMASM_VERSION} {INTER} {ROPI} {RWPI} {SWST} {NOSWST} . @"},contains:[{className:"keyword",begin:"\\b(adc|(qd?|sh?|u[qh]?)?add(8|16)?|usada?8|(q|sh?|u[qh]?)?(as|sa)x|and|adrl?|sbc|rs[bc]|asr|b[lx]?|blx|bxj|cbn?z|tb[bh]|bic|bfc|bfi|[su]bfx|bkpt|cdp2?|clz|clrex|cmp|cmn|cpsi[ed]|cps|setend|dbg|dmb|dsb|eor|isb|it[te]{0,3}|lsl|lsr|ror|rrx|ldm(([id][ab])|f[ds])?|ldr((s|ex)?[bhd])?|movt?|mvn|mra|mar|mul|[us]mull|smul[bwt][bt]|smu[as]d|smmul|smmla|mla|umlaal|smlal?([wbt][bt]|d)|mls|smlsl?[ds]|smc|svc|sev|mia([bt]{2}|ph)?|mrr?c2?|mcrr2?|mrs|msr|orr|orn|pkh(tb|bt)|rbit|rev(16|sh)?|sel|[su]sat(16)?|nop|pop|push|rfe([id][ab])?|stm([id][ab])?|str(ex)?[bhd]?|(qd?)?sub|(sh?|q|u[qh]?)?sub(8|16)|[su]xt(a?h|a?b(16)?)|srs([id][ab])?|swpb?|swi|smi|tst|teq|wfe|wfi|yield)(eq|ne|cs|cc|mi|pl|vs|vc|hi|ls|ge|lt|gt|le|al|hs|lo)?[sptrx]?(?=\\s)"},e,s.QUOTE_STRING_MODE,{className:"string",begin:"'",end:"[^\\\\]'",relevance:0},{className:"title",begin:"\\|",end:"\\|",illegal:"\\n",relevance:0},{className:"number",variants:[{begin:"[#$=]?0x[0-9a-f]+"},{begin:"[#$=]?0b[01]+"},{begin:"[#$=]\\d+"},{begin:"\\b\\d+"}],relevance:0},{className:"symbol",variants:[{begin:"^[ \\t]*[a-z_\\.\\$][a-z0-9_\\.\\$]+:"},{begin:"^[a-z_\\.\\$][a-z0-9_\\.\\$]+"},{begin:"[=#]\\w+"}],relevance:0}]}}}());hljs.registerLanguage("go",function(){"use strict";return function(e){var n={keyword:"break default func interface select case map struct chan else goto package switch const fallthrough if range type continue for import return var go defer bool byte complex64 complex128 float32 float64 int8 int16 int32 int64 string uint8 uint16 uint32 uint64 int uint uintptr rune",literal:"true false iota nil",built_in:"append cap close complex copy imag len make new panic print println real recover delete"};return{name:"Go",aliases:["golang"],keywords:n,illegal:">>|\.\.\.) /},i={className:"subst",begin:/\{/,end:/\}/,keywords:n,illegal:/#/},s={begin:/\{\{/,relevance:0},r={className:"string",contains:[e.BACKSLASH_ESCAPE],variants:[{begin:/(u|b)?r?'''/,end:/'''/,contains:[e.BACKSLASH_ESCAPE,a],relevance:10},{begin:/(u|b)?r?"""/,end:/"""/,contains:[e.BACKSLASH_ESCAPE,a],relevance:10},{begin:/(fr|rf|f)'''/,end:/'''/,contains:[e.BACKSLASH_ESCAPE,a,s,i]},{begin:/(fr|rf|f)"""/,end:/"""/,contains:[e.BACKSLASH_ESCAPE,a,s,i]},{begin:/(u|r|ur)'/,end:/'/,relevance:10},{begin:/(u|r|ur)"/,end:/"/,relevance:10},{begin:/(b|br)'/,end:/'/},{begin:/(b|br)"/,end:/"/},{begin:/(fr|rf|f)'/,end:/'/,contains:[e.BACKSLASH_ESCAPE,s,i]},{begin:/(fr|rf|f)"/,end:/"/,contains:[e.BACKSLASH_ESCAPE,s,i]},e.APOS_STRING_MODE,e.QUOTE_STRING_MODE]},l={className:"number",relevance:0,variants:[{begin:e.BINARY_NUMBER_RE+"[lLjJ]?"},{begin:"\\b(0o[0-7]+)[lLjJ]?"},{begin:e.C_NUMBER_RE+"[lLjJ]?"}]},t={className:"params",variants:[{begin:/\(\s*\)/,skip:!0,className:null},{begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,contains:["self",a,l,r,e.HASH_COMMENT_MODE]}]};return i.contains=[r,l,a],{name:"Python",aliases:["py","gyp","ipython"],keywords:n,illegal:/(<\/|->|\?)|=>/,contains:[a,l,{beginKeywords:"if",relevance:0},r,e.HASH_COMMENT_MODE,{variants:[{className:"function",beginKeywords:"def"},{className:"class",beginKeywords:"class"}],end:/:/,illegal:/[${=;\n,]/,contains:[e.UNDERSCORE_TITLE_MODE,t,{begin:/->/,endsWithParent:!0,keywords:"None"}]},{className:"meta",begin:/^[\t ]*@/,end:/$/},{begin:/\b(print|exec)\(/}]}}}());hljs.registerLanguage("shell",function(){"use strict";return function(s){return{name:"Shell Session",aliases:["console"],contains:[{className:"meta",begin:"^\\s{0,3}[/\\w\\d\\[\\]()@-]*[>%$#]",starts:{end:"$",subLanguage:"bash"}}]}}}());hljs.registerLanguage("scala",function(){"use strict";return function(e){var n={className:"subst",variants:[{begin:"\\$[A-Za-z0-9_]+"},{begin:"\\${",end:"}"}]},a={className:"string",variants:[{begin:'"',end:'"',illegal:"\\n",contains:[e.BACKSLASH_ESCAPE]},{begin:'"""',end:'"""',relevance:10},{begin:'[a-z]+"',end:'"',illegal:"\\n",contains:[e.BACKSLASH_ESCAPE,n]},{className:"string",begin:'[a-z]+"""',end:'"""',contains:[n],relevance:10}]},s={className:"type",begin:"\\b[A-Z][A-Za-z0-9_]*",relevance:0},t={className:"title",begin:/[^0-9\n\t "'(),.`{}\[\]:;][^\n\t "'(),.`{}\[\]:;]+|[^0-9\n\t "'(),.`{}\[\]:;=]/,relevance:0},i={className:"class",beginKeywords:"class object trait type",end:/[:={\[\n;]/,excludeEnd:!0,contains:[{beginKeywords:"extends with",relevance:10},{begin:/\[/,end:/\]/,excludeBegin:!0,excludeEnd:!0,relevance:0,contains:[s]},{className:"params",begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,relevance:0,contains:[s]},t]},l={className:"function",beginKeywords:"def",end:/[:={\[(\n;]/,excludeEnd:!0,contains:[t]};return{name:"Scala",keywords:{literal:"true false null",keyword:"type yield lazy override def with val var sealed abstract private trait object if forSome for while throw finally protected extends import final return else break new catch super class case package default try this match continue throws implicit"},contains:[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,a,{className:"symbol",begin:"'\\w[\\w\\d_]*(?!')"},s,l,i,e.C_NUMBER_MODE,{className:"meta",begin:"@[A-Za-z]+"}]}}}());hljs.registerLanguage("julia",function(){"use strict";return function(e){var r="[A-Za-z_\\u00A1-\\uFFFF][A-Za-z_0-9\\u00A1-\\uFFFF]*",t={$pattern:r,keyword:"in isa where baremodule begin break catch ccall const continue do else elseif end export false finally for function global if import importall let local macro module quote return true try using while type immutable abstract bitstype typealias ",literal:"true false ARGS C_NULL DevNull ENDIAN_BOM ENV I Inf Inf16 Inf32 Inf64 InsertionSort JULIA_HOME LOAD_PATH MergeSort NaN NaN16 NaN32 NaN64 PROGRAM_FILE QuickSort RoundDown RoundFromZero RoundNearest RoundNearestTiesAway RoundNearestTiesUp RoundToZero RoundUp STDERR STDIN STDOUT VERSION catalan e|0 eu|0 eulergamma golden im nothing pi γ π φ ",built_in:"ANY AbstractArray AbstractChannel AbstractFloat AbstractMatrix AbstractRNG AbstractSerializer AbstractSet AbstractSparseArray AbstractSparseMatrix AbstractSparseVector AbstractString AbstractUnitRange AbstractVecOrMat AbstractVector Any ArgumentError Array AssertionError Associative Base64DecodePipe Base64EncodePipe Bidiagonal BigFloat BigInt BitArray BitMatrix BitVector Bool BoundsError BufferStream CachingPool CapturedException CartesianIndex CartesianRange Cchar Cdouble Cfloat Channel Char Cint Cintmax_t Clong Clonglong ClusterManager Cmd CodeInfo Colon Complex Complex128 Complex32 Complex64 CompositeException Condition ConjArray ConjMatrix ConjVector Cptrdiff_t Cshort Csize_t Cssize_t Cstring Cuchar Cuint Cuintmax_t Culong Culonglong Cushort Cwchar_t Cwstring DataType Date DateFormat DateTime DenseArray DenseMatrix DenseVecOrMat DenseVector Diagonal Dict DimensionMismatch Dims DirectIndexString Display DivideError DomainError EOFError EachLine Enum Enumerate ErrorException Exception ExponentialBackOff Expr Factorization FileMonitor Float16 Float32 Float64 Function Future GlobalRef GotoNode HTML Hermitian IO IOBuffer IOContext IOStream IPAddr IPv4 IPv6 IndexCartesian IndexLinear IndexStyle InexactError InitError Int Int128 Int16 Int32 Int64 Int8 IntSet Integer InterruptException InvalidStateException Irrational KeyError LabelNode LinSpace LineNumberNode LoadError LowerTriangular MIME Matrix MersenneTwister Method MethodError MethodTable Module NTuple NewvarNode NullException Nullable Number ObjectIdDict OrdinalRange OutOfMemoryError OverflowError Pair ParseError PartialQuickSort PermutedDimsArray Pipe PollingFileWatcher ProcessExitedException Ptr QuoteNode RandomDevice Range RangeIndex Rational RawFD ReadOnlyMemoryError Real ReentrantLock Ref Regex RegexMatch RemoteChannel RemoteException RevString RoundingMode RowVector SSAValue SegmentationFault SerializationState Set SharedArray SharedMatrix SharedVector Signed SimpleVector Slot SlotNumber SparseMatrixCSC SparseVector StackFrame StackOverflowError StackTrace StepRange StepRangeLen StridedArray StridedMatrix StridedVecOrMat StridedVector String SubArray SubString SymTridiagonal Symbol Symmetric SystemError TCPSocket Task Text TextDisplay Timer Tridiagonal Tuple Type TypeError TypeMapEntry TypeMapLevel TypeName TypeVar TypedSlot UDPSocket UInt UInt128 UInt16 UInt32 UInt64 UInt8 UndefRefError UndefVarError UnicodeError UniformScaling Union UnionAll UnitRange Unsigned UpperTriangular Val Vararg VecElement VecOrMat Vector VersionNumber Void WeakKeyDict WeakRef WorkerConfig WorkerPool "},a={keywords:t,illegal:/<\//},n={className:"subst",begin:/\$\(/,end:/\)/,keywords:t},o={className:"variable",begin:"\\$"+r},i={className:"string",contains:[e.BACKSLASH_ESCAPE,n,o],variants:[{begin:/\w*"""/,end:/"""\w*/,relevance:10},{begin:/\w*"/,end:/"\w*/}]},l={className:"string",contains:[e.BACKSLASH_ESCAPE,n,o],begin:"`",end:"`"},s={className:"meta",begin:"@"+r};return a.name="Julia",a.contains=[{className:"number",begin:/(\b0x[\d_]*(\.[\d_]*)?|0x\.\d[\d_]*)p[-+]?\d+|\b0[box][a-fA-F0-9][a-fA-F0-9_]*|(\b\d[\d_]*(\.[\d_]*)?|\.\d[\d_]*)([eEfF][-+]?\d+)?/,relevance:0},{className:"string",begin:/'(.|\\[xXuU][a-zA-Z0-9]+)'/},i,l,s,{className:"comment",variants:[{begin:"#=",end:"=#",relevance:10},{begin:"#",end:"$"}]},e.HASH_COMMENT_MODE,{className:"keyword",begin:"\\b(((abstract|primitive)\\s+)type|(mutable\\s+)?struct)\\b"},{begin:/<:/}],n.contains=a.contains,a}}());hljs.registerLanguage("php-template",function(){"use strict";return function(n){return{name:"PHP template",subLanguage:"xml",contains:[{begin:/<\?(php|=)?/,end:/\?>/,subLanguage:"php",contains:[{begin:"/\\*",end:"\\*/",skip:!0},{begin:'b"',end:'"',skip:!0},{begin:"b'",end:"'",skip:!0},n.inherit(n.APOS_STRING_MODE,{illegal:null,className:null,contains:null,skip:!0}),n.inherit(n.QUOTE_STRING_MODE,{illegal:null,className:null,contains:null,skip:!0})]}]}}}());hljs.registerLanguage("scss",function(){"use strict";return function(e){var t={className:"variable",begin:"(\\$[a-zA-Z-][a-zA-Z0-9_-]*)\\b"},i={className:"number",begin:"#[0-9A-Fa-f]+"};return e.CSS_NUMBER_MODE,e.QUOTE_STRING_MODE,e.APOS_STRING_MODE,e.C_BLOCK_COMMENT_MODE,{name:"SCSS",case_insensitive:!0,illegal:"[=/|']",contains:[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,{className:"selector-id",begin:"\\#[A-Za-z0-9_-]+",relevance:0},{className:"selector-class",begin:"\\.[A-Za-z0-9_-]+",relevance:0},{className:"selector-attr",begin:"\\[",end:"\\]",illegal:"$"},{className:"selector-tag",begin:"\\b(a|abbr|acronym|address|area|article|aside|audio|b|base|big|blockquote|body|br|button|canvas|caption|cite|code|col|colgroup|command|datalist|dd|del|details|dfn|div|dl|dt|em|embed|fieldset|figcaption|figure|footer|form|frame|frameset|(h[1-6])|head|header|hgroup|hr|html|i|iframe|img|input|ins|kbd|keygen|label|legend|li|link|map|mark|meta|meter|nav|noframes|noscript|object|ol|optgroup|option|output|p|param|pre|progress|q|rp|rt|ruby|samp|script|section|select|small|span|strike|strong|style|sub|sup|table|tbody|td|textarea|tfoot|th|thead|time|title|tr|tt|ul|var|video)\\b",relevance:0},{className:"selector-pseudo",begin:":(visited|valid|root|right|required|read-write|read-only|out-range|optional|only-of-type|only-child|nth-of-type|nth-last-of-type|nth-last-child|nth-child|not|link|left|last-of-type|last-child|lang|invalid|indeterminate|in-range|hover|focus|first-of-type|first-line|first-letter|first-child|first|enabled|empty|disabled|default|checked|before|after|active)"},{className:"selector-pseudo",begin:"::(after|before|choices|first-letter|first-line|repeat-index|repeat-item|selection|value)"},t,{className:"attribute",begin:"\\b(src|z-index|word-wrap|word-spacing|word-break|width|widows|white-space|visibility|vertical-align|unicode-bidi|transition-timing-function|transition-property|transition-duration|transition-delay|transition|transform-style|transform-origin|transform|top|text-underline-position|text-transform|text-shadow|text-rendering|text-overflow|text-indent|text-decoration-style|text-decoration-line|text-decoration-color|text-decoration|text-align-last|text-align|tab-size|table-layout|right|resize|quotes|position|pointer-events|perspective-origin|perspective|page-break-inside|page-break-before|page-break-after|padding-top|padding-right|padding-left|padding-bottom|padding|overflow-y|overflow-x|overflow-wrap|overflow|outline-width|outline-style|outline-offset|outline-color|outline|orphans|order|opacity|object-position|object-fit|normal|none|nav-up|nav-right|nav-left|nav-index|nav-down|min-width|min-height|max-width|max-height|mask|marks|margin-top|margin-right|margin-left|margin-bottom|margin|list-style-type|list-style-position|list-style-image|list-style|line-height|letter-spacing|left|justify-content|initial|inherit|ime-mode|image-orientation|image-resolution|image-rendering|icon|hyphens|height|font-weight|font-variant-ligatures|font-variant|font-style|font-stretch|font-size-adjust|font-size|font-language-override|font-kerning|font-feature-settings|font-family|font|float|flex-wrap|flex-shrink|flex-grow|flex-flow|flex-direction|flex-basis|flex|filter|empty-cells|display|direction|cursor|counter-reset|counter-increment|content|column-width|column-span|column-rule-width|column-rule-style|column-rule-color|column-rule|column-gap|column-fill|column-count|columns|color|clip-path|clip|clear|caption-side|break-inside|break-before|break-after|box-sizing|box-shadow|box-decoration-break|bottom|border-width|border-top-width|border-top-style|border-top-right-radius|border-top-left-radius|border-top-color|border-top|border-style|border-spacing|border-right-width|border-right-style|border-right-color|border-right|border-radius|border-left-width|border-left-style|border-left-color|border-left|border-image-width|border-image-source|border-image-slice|border-image-repeat|border-image-outset|border-image|border-color|border-collapse|border-bottom-width|border-bottom-style|border-bottom-right-radius|border-bottom-left-radius|border-bottom-color|border-bottom|border|background-size|background-repeat|background-position|background-origin|background-image|background-color|background-clip|background-attachment|background-blend-mode|background|backface-visibility|auto|animation-timing-function|animation-play-state|animation-name|animation-iteration-count|animation-fill-mode|animation-duration|animation-direction|animation-delay|animation|align-self|align-items|align-content)\\b",illegal:"[^\\s]"},{begin:"\\b(whitespace|wait|w-resize|visible|vertical-text|vertical-ideographic|uppercase|upper-roman|upper-alpha|underline|transparent|top|thin|thick|text|text-top|text-bottom|tb-rl|table-header-group|table-footer-group|sw-resize|super|strict|static|square|solid|small-caps|separate|se-resize|scroll|s-resize|rtl|row-resize|ridge|right|repeat|repeat-y|repeat-x|relative|progress|pointer|overline|outside|outset|oblique|nowrap|not-allowed|normal|none|nw-resize|no-repeat|no-drop|newspaper|ne-resize|n-resize|move|middle|medium|ltr|lr-tb|lowercase|lower-roman|lower-alpha|loose|list-item|line|line-through|line-edge|lighter|left|keep-all|justify|italic|inter-word|inter-ideograph|inside|inset|inline|inline-block|inherit|inactive|ideograph-space|ideograph-parenthesis|ideograph-numeric|ideograph-alpha|horizontal|hidden|help|hand|groove|fixed|ellipsis|e-resize|double|dotted|distribute|distribute-space|distribute-letter|distribute-all-lines|disc|disabled|default|decimal|dashed|crosshair|collapse|col-resize|circle|char|center|capitalize|break-word|break-all|bottom|both|bolder|bold|block|bidi-override|below|baseline|auto|always|all-scroll|absolute|table|table-cell)\\b"},{begin:":",end:";",contains:[t,i,e.CSS_NUMBER_MODE,e.QUOTE_STRING_MODE,e.APOS_STRING_MODE,{className:"meta",begin:"!important"}]},{begin:"@(page|font-face)",lexemes:"@[a-z-]+",keywords:"@page @font-face"},{begin:"@",end:"[{;]",returnBegin:!0,keywords:"and or not only",contains:[{begin:"@[a-z-]+",className:"keyword"},t,e.QUOTE_STRING_MODE,e.APOS_STRING_MODE,i,e.CSS_NUMBER_MODE]}]}}}());hljs.registerLanguage("r",function(){"use strict";return function(e){var n="([a-zA-Z]|\\.[a-zA-Z.])[a-zA-Z0-9._]*";return{name:"R",contains:[e.HASH_COMMENT_MODE,{begin:n,keywords:{$pattern:n,keyword:"function if in break next repeat else for return switch while try tryCatch stop warning require library attach detach source setMethod setGeneric setGroupGeneric setClass ...",literal:"NULL NA TRUE FALSE T F Inf NaN NA_integer_|10 NA_real_|10 NA_character_|10 NA_complex_|10"},relevance:0},{className:"number",begin:"0[xX][0-9a-fA-F]+[Li]?\\b",relevance:0},{className:"number",begin:"\\d+(?:[eE][+\\-]?\\d*)?L\\b",relevance:0},{className:"number",begin:"\\d+\\.(?!\\d)(?:i\\b)?",relevance:0},{className:"number",begin:"\\d+(?:\\.\\d*)?(?:[eE][+\\-]?\\d*)?i?\\b",relevance:0},{className:"number",begin:"\\.\\d+(?:[eE][+\\-]?\\d*)?i?\\b",relevance:0},{begin:"`",end:"`",relevance:0},{className:"string",contains:[e.BACKSLASH_ESCAPE],variants:[{begin:'"',end:'"'},{begin:"'",end:"'"}]}]}}}());hljs.registerLanguage("sql",function(){"use strict";return function(e){var t=e.COMMENT("--","$");return{name:"SQL",case_insensitive:!0,illegal:/[<>{}*]/,contains:[{beginKeywords:"begin end start commit rollback savepoint lock alter create drop rename call delete do handler insert load replace select truncate update set show pragma grant merge describe use explain help declare prepare execute deallocate release unlock purge reset change stop analyze cache flush optimize repair kill install uninstall checksum restore check backup revoke comment values with",end:/;/,endsWithParent:!0,keywords:{$pattern:/[\w\.]+/,keyword:"as abort abs absolute acc acce accep accept access accessed accessible account acos action activate add addtime admin administer advanced advise aes_decrypt aes_encrypt after agent aggregate ali alia alias all allocate allow alter always analyze ancillary and anti any anydata anydataset anyschema anytype apply archive archived archivelog are as asc ascii asin assembly assertion associate asynchronous at atan atn2 attr attri attrib attribu attribut attribute attributes audit authenticated authentication authid authors auto autoallocate autodblink autoextend automatic availability avg backup badfile basicfile before begin beginning benchmark between bfile bfile_base big bigfile bin binary_double binary_float binlog bit_and bit_count bit_length bit_or bit_xor bitmap blob_base block blocksize body both bound bucket buffer_cache buffer_pool build bulk by byte byteordermark bytes cache caching call calling cancel capacity cascade cascaded case cast catalog category ceil ceiling chain change changed char_base char_length character_length characters characterset charindex charset charsetform charsetid check checksum checksum_agg child choose chr chunk class cleanup clear client clob clob_base clone close cluster_id cluster_probability cluster_set clustering coalesce coercibility col collate collation collect colu colum column column_value columns columns_updated comment commit compact compatibility compiled complete composite_limit compound compress compute concat concat_ws concurrent confirm conn connec connect connect_by_iscycle connect_by_isleaf connect_by_root connect_time connection consider consistent constant constraint constraints constructor container content contents context contributors controlfile conv convert convert_tz corr corr_k corr_s corresponding corruption cos cost count count_big counted covar_pop covar_samp cpu_per_call cpu_per_session crc32 create creation critical cross cube cume_dist curdate current current_date current_time current_timestamp current_user cursor curtime customdatum cycle data database databases datafile datafiles datalength date_add date_cache date_format date_sub dateadd datediff datefromparts datename datepart datetime2fromparts day day_to_second dayname dayofmonth dayofweek dayofyear days db_role_change dbtimezone ddl deallocate declare decode decompose decrement decrypt deduplicate def defa defau defaul default defaults deferred defi defin define degrees delayed delegate delete delete_all delimited demand dense_rank depth dequeue des_decrypt des_encrypt des_key_file desc descr descri describ describe descriptor deterministic diagnostics difference dimension direct_load directory disable disable_all disallow disassociate discardfile disconnect diskgroup distinct distinctrow distribute distributed div do document domain dotnet double downgrade drop dumpfile duplicate duration each edition editionable editions element ellipsis else elsif elt empty enable enable_all enclosed encode encoding encrypt end end-exec endian enforced engine engines enqueue enterprise entityescaping eomonth error errors escaped evalname evaluate event eventdata events except exception exceptions exchange exclude excluding execu execut execute exempt exists exit exp expire explain explode export export_set extended extent external external_1 external_2 externally extract failed failed_login_attempts failover failure far fast feature_set feature_value fetch field fields file file_name_convert filesystem_like_logging final finish first first_value fixed flash_cache flashback floor flush following follows for forall force foreign form forma format found found_rows freelist freelists freepools fresh from from_base64 from_days ftp full function general generated get get_format get_lock getdate getutcdate global global_name globally go goto grant grants greatest group group_concat group_id grouping grouping_id groups gtid_subtract guarantee guard handler hash hashkeys having hea head headi headin heading heap help hex hierarchy high high_priority hosts hour hours http id ident_current ident_incr ident_seed identified identity idle_time if ifnull ignore iif ilike ilm immediate import in include including increment index indexes indexing indextype indicator indices inet6_aton inet6_ntoa inet_aton inet_ntoa infile initial initialized initially initrans inmemory inner innodb input insert install instance instantiable instr interface interleaved intersect into invalidate invisible is is_free_lock is_ipv4 is_ipv4_compat is_not is_not_null is_used_lock isdate isnull isolation iterate java join json json_exists keep keep_duplicates key keys kill language large last last_day last_insert_id last_value lateral lax lcase lead leading least leaves left len lenght length less level levels library like like2 like4 likec limit lines link list listagg little ln load load_file lob lobs local localtime localtimestamp locate locator lock locked log log10 log2 logfile logfiles logging logical logical_reads_per_call logoff logon logs long loop low low_priority lower lpad lrtrim ltrim main make_set makedate maketime managed management manual map mapping mask master master_pos_wait match matched materialized max maxextents maximize maxinstances maxlen maxlogfiles maxloghistory maxlogmembers maxsize maxtrans md5 measures median medium member memcompress memory merge microsecond mid migration min minextents minimum mining minus minute minutes minvalue missing mod mode model modification modify module monitoring month months mount move movement multiset mutex name name_const names nan national native natural nav nchar nclob nested never new newline next nextval no no_write_to_binlog noarchivelog noaudit nobadfile nocheck nocompress nocopy nocycle nodelay nodiscardfile noentityescaping noguarantee nokeep nologfile nomapping nomaxvalue nominimize nominvalue nomonitoring none noneditionable nonschema noorder nopr nopro noprom nopromp noprompt norely noresetlogs noreverse normal norowdependencies noschemacheck noswitch not nothing notice notnull notrim novalidate now nowait nth_value nullif nulls num numb numbe nvarchar nvarchar2 object ocicoll ocidate ocidatetime ociduration ociinterval ociloblocator ocinumber ociref ocirefcursor ocirowid ocistring ocitype oct octet_length of off offline offset oid oidindex old on online only opaque open operations operator optimal optimize option optionally or oracle oracle_date oradata ord ordaudio orddicom orddoc order ordimage ordinality ordvideo organization orlany orlvary out outer outfile outline output over overflow overriding package pad parallel parallel_enable parameters parent parse partial partition partitions pascal passing password password_grace_time password_lock_time password_reuse_max password_reuse_time password_verify_function patch path patindex pctincrease pctthreshold pctused pctversion percent percent_rank percentile_cont percentile_disc performance period period_add period_diff permanent physical pi pipe pipelined pivot pluggable plugin policy position post_transaction pow power pragma prebuilt precedes preceding precision prediction prediction_cost prediction_details prediction_probability prediction_set prepare present preserve prior priority private private_sga privileges procedural procedure procedure_analyze processlist profiles project prompt protection public publishingservername purge quarter query quick quiesce quota quotename radians raise rand range rank raw read reads readsize rebuild record records recover recovery recursive recycle redo reduced ref reference referenced references referencing refresh regexp_like register regr_avgx regr_avgy regr_count regr_intercept regr_r2 regr_slope regr_sxx regr_sxy reject rekey relational relative relaylog release release_lock relies_on relocate rely rem remainder rename repair repeat replace replicate replication required reset resetlogs resize resource respect restore restricted result result_cache resumable resume retention return returning returns reuse reverse revoke right rlike role roles rollback rolling rollup round row row_count rowdependencies rowid rownum rows rtrim rules safe salt sample save savepoint sb1 sb2 sb4 scan schema schemacheck scn scope scroll sdo_georaster sdo_topo_geometry search sec_to_time second seconds section securefile security seed segment select self semi sequence sequential serializable server servererror session session_user sessions_per_user set sets settings sha sha1 sha2 share shared shared_pool short show shrink shutdown si_averagecolor si_colorhistogram si_featurelist si_positionalcolor si_stillimage si_texture siblings sid sign sin size size_t sizes skip slave sleep smalldatetimefromparts smallfile snapshot some soname sort soundex source space sparse spfile split sql sql_big_result sql_buffer_result sql_cache sql_calc_found_rows sql_small_result sql_variant_property sqlcode sqldata sqlerror sqlname sqlstate sqrt square standalone standby start starting startup statement static statistics stats_binomial_test stats_crosstab stats_ks_test stats_mode stats_mw_test stats_one_way_anova stats_t_test_ stats_t_test_indep stats_t_test_one stats_t_test_paired stats_wsr_test status std stddev stddev_pop stddev_samp stdev stop storage store stored str str_to_date straight_join strcmp strict string struct stuff style subdate subpartition subpartitions substitutable substr substring subtime subtring_index subtype success sum suspend switch switchoffset switchover sync synchronous synonym sys sys_xmlagg sysasm sysaux sysdate sysdatetimeoffset sysdba sysoper system system_user sysutcdatetime table tables tablespace tablesample tan tdo template temporary terminated tertiary_weights test than then thread through tier ties time time_format time_zone timediff timefromparts timeout timestamp timestampadd timestampdiff timezone_abbr timezone_minute timezone_region to to_base64 to_date to_days to_seconds todatetimeoffset trace tracking transaction transactional translate translation treat trigger trigger_nestlevel triggers trim truncate try_cast try_convert try_parse type ub1 ub2 ub4 ucase unarchived unbounded uncompress under undo unhex unicode uniform uninstall union unique unix_timestamp unknown unlimited unlock unnest unpivot unrecoverable unsafe unsigned until untrusted unusable unused update updated upgrade upped upper upsert url urowid usable usage use use_stored_outlines user user_data user_resources users using utc_date utc_timestamp uuid uuid_short validate validate_password_strength validation valist value values var var_samp varcharc vari varia variab variabl variable variables variance varp varraw varrawc varray verify version versions view virtual visible void wait wallet warning warnings week weekday weekofyear wellformed when whene whenev wheneve whenever where while whitespace window with within without work wrapped xdb xml xmlagg xmlattributes xmlcast xmlcolattval xmlelement xmlexists xmlforest xmlindex xmlnamespaces xmlpi xmlquery xmlroot xmlschema xmlserialize xmltable xmltype xor year year_to_month years yearweek",literal:"true false null unknown",built_in:"array bigint binary bit blob bool boolean char character date dec decimal float int int8 integer interval number numeric real record serial serial8 smallint text time timestamp tinyint varchar varchar2 varying void"},contains:[{className:"string",begin:"'",end:"'",contains:[{begin:"''"}]},{className:"string",begin:'"',end:'"',contains:[{begin:'""'}]},{className:"string",begin:"`",end:"`"},e.C_NUMBER_MODE,e.C_BLOCK_COMMENT_MODE,t,e.HASH_COMMENT_MODE]},e.C_BLOCK_COMMENT_MODE,t,e.HASH_COMMENT_MODE]}}}());hljs.registerLanguage("c",function(){"use strict";return function(e){var n=e.getLanguage("c-like").rawDefinition();return n.name="C",n.aliases=["c","h"],n}}());hljs.registerLanguage("json",function(){"use strict";return function(n){var e={literal:"true false null"},i=[n.C_LINE_COMMENT_MODE,n.C_BLOCK_COMMENT_MODE],t=[n.QUOTE_STRING_MODE,n.C_NUMBER_MODE],a={end:",",endsWithParent:!0,excludeEnd:!0,contains:t,keywords:e},l={begin:"{",end:"}",contains:[{className:"attr",begin:/"/,end:/"/,contains:[n.BACKSLASH_ESCAPE],illegal:"\\n"},n.inherit(a,{begin:/:/})].concat(i),illegal:"\\S"},s={begin:"\\[",end:"\\]",contains:[n.inherit(a)],illegal:"\\S"};return t.push(l,s),i.forEach((function(n){t.push(n)})),{name:"JSON",contains:t,keywords:e,illegal:"\\S"}}}());hljs.registerLanguage("python-repl",function(){"use strict";return function(n){return{aliases:["pycon"],contains:[{className:"meta",starts:{end:/ |$/,starts:{end:"$",subLanguage:"python"}},variants:[{begin:/^>>>(?=[ ]|$)/},{begin:/^\.\.\.(?=[ ]|$)/}]}]}}}());hljs.registerLanguage("markdown",function(){"use strict";return function(n){const e={begin:"<",end:">",subLanguage:"xml",relevance:0},a={begin:"\\[.+?\\][\\(\\[].*?[\\)\\]]",returnBegin:!0,contains:[{className:"string",begin:"\\[",end:"\\]",excludeBegin:!0,returnEnd:!0,relevance:0},{className:"link",begin:"\\]\\(",end:"\\)",excludeBegin:!0,excludeEnd:!0},{className:"symbol",begin:"\\]\\[",end:"\\]",excludeBegin:!0,excludeEnd:!0}],relevance:10},i={className:"strong",contains:[],variants:[{begin:/_{2}/,end:/_{2}/},{begin:/\*{2}/,end:/\*{2}/}]},s={className:"emphasis",contains:[],variants:[{begin:/\*(?!\*)/,end:/\*/},{begin:/_(?!_)/,end:/_/,relevance:0}]};i.contains.push(s),s.contains.push(i);var c=[e,a];return i.contains=i.contains.concat(c),s.contains=s.contains.concat(c),{name:"Markdown",aliases:["md","mkdown","mkd"],contains:[{className:"section",variants:[{begin:"^#{1,6}",end:"$",contains:c=c.concat(i,s)},{begin:"(?=^.+?\\n[=-]{2,}$)",contains:[{begin:"^[=-]*$"},{begin:"^",end:"\\n",contains:c}]}]},e,{className:"bullet",begin:"^[ \t]*([*+-]|(\\d+\\.))(?=\\s+)",end:"\\s+",excludeEnd:!0},i,s,{className:"quote",begin:"^>\\s+",contains:c,end:"$"},{className:"code",variants:[{begin:"(`{3,})(.|\\n)*?\\1`*[ ]*"},{begin:"(~{3,})(.|\\n)*?\\1~*[ ]*"},{begin:"```",end:"```+[ ]*$"},{begin:"~~~",end:"~~~+[ ]*$"},{begin:"`.+?`"},{begin:"(?=^( {4}|\\t))",contains:[{begin:"^( {4}|\\t)",end:"(\\n)$"}],relevance:0}]},{begin:"^[-\\*]{3,}",end:"$"},a,{begin:/^\[[^\n]+\]:/,returnBegin:!0,contains:[{className:"symbol",begin:/\[/,end:/\]/,excludeBegin:!0,excludeEnd:!0},{className:"link",begin:/:\s*/,end:/$/,excludeBegin:!0}]}]}}}());hljs.registerLanguage("javascript",function(){"use strict";const e=["as","in","of","if","for","while","finally","var","new","function","do","return","void","else","break","catch","instanceof","with","throw","case","default","try","switch","continue","typeof","delete","let","yield","const","class","debugger","async","await","static","import","from","export","extends"],n=["true","false","null","undefined","NaN","Infinity"],a=[].concat(["setInterval","setTimeout","clearInterval","clearTimeout","require","exports","eval","isFinite","isNaN","parseFloat","parseInt","decodeURI","decodeURIComponent","encodeURI","encodeURIComponent","escape","unescape"],["arguments","this","super","console","window","document","localStorage","module","global"],["Intl","DataView","Number","Math","Date","String","RegExp","Object","Function","Boolean","Error","Symbol","Set","Map","WeakSet","WeakMap","Proxy","Reflect","JSON","Promise","Float64Array","Int16Array","Int32Array","Int8Array","Uint16Array","Uint32Array","Float32Array","Array","Uint8Array","Uint8ClampedArray","ArrayBuffer"],["EvalError","InternalError","RangeError","ReferenceError","SyntaxError","TypeError","URIError"]);function s(e){return r("(?=",e,")")}function r(...e){return e.map(e=>(function(e){return e?"string"==typeof e?e:e.source:null})(e)).join("")}return function(t){var i="[A-Za-z$_][0-9A-Za-z$_]*",c={begin:/<[A-Za-z0-9\\._:-]+/,end:/\/[A-Za-z0-9\\._:-]+>|\/>/},o={$pattern:"[A-Za-z$_][0-9A-Za-z$_]*",keyword:e.join(" "),literal:n.join(" "),built_in:a.join(" ")},l={className:"number",variants:[{begin:"\\b(0[bB][01]+)n?"},{begin:"\\b(0[oO][0-7]+)n?"},{begin:t.C_NUMBER_RE+"n?"}],relevance:0},E={className:"subst",begin:"\\$\\{",end:"\\}",keywords:o,contains:[]},d={begin:"html`",end:"",starts:{end:"`",returnEnd:!1,contains:[t.BACKSLASH_ESCAPE,E],subLanguage:"xml"}},g={begin:"css`",end:"",starts:{end:"`",returnEnd:!1,contains:[t.BACKSLASH_ESCAPE,E],subLanguage:"css"}},u={className:"string",begin:"`",end:"`",contains:[t.BACKSLASH_ESCAPE,E]};E.contains=[t.APOS_STRING_MODE,t.QUOTE_STRING_MODE,d,g,u,l,t.REGEXP_MODE];var b=E.contains.concat([{begin:/\(/,end:/\)/,contains:["self"].concat(E.contains,[t.C_BLOCK_COMMENT_MODE,t.C_LINE_COMMENT_MODE])},t.C_BLOCK_COMMENT_MODE,t.C_LINE_COMMENT_MODE]),_={className:"params",begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,contains:b};return{name:"JavaScript",aliases:["js","jsx","mjs","cjs"],keywords:o,contains:[t.SHEBANG({binary:"node",relevance:5}),{className:"meta",relevance:10,begin:/^\s*['"]use (strict|asm)['"]/},t.APOS_STRING_MODE,t.QUOTE_STRING_MODE,d,g,u,t.C_LINE_COMMENT_MODE,t.COMMENT("/\\*\\*","\\*/",{relevance:0,contains:[{className:"doctag",begin:"@[A-Za-z]+",contains:[{className:"type",begin:"\\{",end:"\\}",relevance:0},{className:"variable",begin:i+"(?=\\s*(-)|$)",endsParent:!0,relevance:0},{begin:/(?=[^\n])\s/,relevance:0}]}]}),t.C_BLOCK_COMMENT_MODE,l,{begin:r(/[{,\n]\s*/,s(r(/(((\/\/.*)|(\/\*(.|\n)*\*\/))\s*)*/,i+"\\s*:"))),relevance:0,contains:[{className:"attr",begin:i+s("\\s*:"),relevance:0}]},{begin:"("+t.RE_STARTERS_RE+"|\\b(case|return|throw)\\b)\\s*",keywords:"return throw case",contains:[t.C_LINE_COMMENT_MODE,t.C_BLOCK_COMMENT_MODE,t.REGEXP_MODE,{className:"function",begin:"(\\([^(]*(\\([^(]*(\\([^(]*\\))?\\))?\\)|"+t.UNDERSCORE_IDENT_RE+")\\s*=>",returnBegin:!0,end:"\\s*=>",contains:[{className:"params",variants:[{begin:t.UNDERSCORE_IDENT_RE},{className:null,begin:/\(\s*\)/,skip:!0},{begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:o,contains:b}]}]},{begin:/,/,relevance:0},{className:"",begin:/\s/,end:/\s*/,skip:!0},{variants:[{begin:"<>",end:""},{begin:c.begin,end:c.end}],subLanguage:"xml",contains:[{begin:c.begin,end:c.end,skip:!0,contains:["self"]}]}],relevance:0},{className:"function",beginKeywords:"function",end:/\{/,excludeEnd:!0,contains:[t.inherit(t.TITLE_MODE,{begin:i}),_],illegal:/\[|%/},{begin:/\$[(.]/},t.METHOD_GUARD,{className:"class",beginKeywords:"class",end:/[{;=]/,excludeEnd:!0,illegal:/[:"\[\]]/,contains:[{beginKeywords:"extends"},t.UNDERSCORE_TITLE_MODE]},{beginKeywords:"constructor",end:/\{/,excludeEnd:!0},{begin:"(get|set)\\s+(?="+i+"\\()",end:/{/,keywords:"get set",contains:[t.inherit(t.TITLE_MODE,{begin:i}),{begin:/\(\)/},_]}],illegal:/#(?!!)/}}}());hljs.registerLanguage("typescript",function(){"use strict";const e=["as","in","of","if","for","while","finally","var","new","function","do","return","void","else","break","catch","instanceof","with","throw","case","default","try","switch","continue","typeof","delete","let","yield","const","class","debugger","async","await","static","import","from","export","extends"],n=["true","false","null","undefined","NaN","Infinity"],a=[].concat(["setInterval","setTimeout","clearInterval","clearTimeout","require","exports","eval","isFinite","isNaN","parseFloat","parseInt","decodeURI","decodeURIComponent","encodeURI","encodeURIComponent","escape","unescape"],["arguments","this","super","console","window","document","localStorage","module","global"],["Intl","DataView","Number","Math","Date","String","RegExp","Object","Function","Boolean","Error","Symbol","Set","Map","WeakSet","WeakMap","Proxy","Reflect","JSON","Promise","Float64Array","Int16Array","Int32Array","Int8Array","Uint16Array","Uint32Array","Float32Array","Array","Uint8Array","Uint8ClampedArray","ArrayBuffer"],["EvalError","InternalError","RangeError","ReferenceError","SyntaxError","TypeError","URIError"]);return function(r){var t={$pattern:"[A-Za-z$_][0-9A-Za-z$_]*",keyword:e.concat(["type","namespace","typedef","interface","public","private","protected","implements","declare","abstract","readonly"]).join(" "),literal:n.join(" "),built_in:a.concat(["any","void","number","boolean","string","object","never","enum"]).join(" ")},s={className:"meta",begin:"@[A-Za-z$_][0-9A-Za-z$_]*"},i={className:"number",variants:[{begin:"\\b(0[bB][01]+)n?"},{begin:"\\b(0[oO][0-7]+)n?"},{begin:r.C_NUMBER_RE+"n?"}],relevance:0},o={className:"subst",begin:"\\$\\{",end:"\\}",keywords:t,contains:[]},c={begin:"html`",end:"",starts:{end:"`",returnEnd:!1,contains:[r.BACKSLASH_ESCAPE,o],subLanguage:"xml"}},l={begin:"css`",end:"",starts:{end:"`",returnEnd:!1,contains:[r.BACKSLASH_ESCAPE,o],subLanguage:"css"}},E={className:"string",begin:"`",end:"`",contains:[r.BACKSLASH_ESCAPE,o]};o.contains=[r.APOS_STRING_MODE,r.QUOTE_STRING_MODE,c,l,E,i,r.REGEXP_MODE];var d={begin:"\\(",end:/\)/,keywords:t,contains:["self",r.QUOTE_STRING_MODE,r.APOS_STRING_MODE,r.NUMBER_MODE]},u={className:"params",begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:t,contains:[r.C_LINE_COMMENT_MODE,r.C_BLOCK_COMMENT_MODE,s,d]};return{name:"TypeScript",aliases:["ts"],keywords:t,contains:[r.SHEBANG(),{className:"meta",begin:/^\s*['"]use strict['"]/},r.APOS_STRING_MODE,r.QUOTE_STRING_MODE,c,l,E,r.C_LINE_COMMENT_MODE,r.C_BLOCK_COMMENT_MODE,i,{begin:"("+r.RE_STARTERS_RE+"|\\b(case|return|throw)\\b)\\s*",keywords:"return throw case",contains:[r.C_LINE_COMMENT_MODE,r.C_BLOCK_COMMENT_MODE,r.REGEXP_MODE,{className:"function",begin:"(\\([^(]*(\\([^(]*(\\([^(]*\\))?\\))?\\)|"+r.UNDERSCORE_IDENT_RE+")\\s*=>",returnBegin:!0,end:"\\s*=>",contains:[{className:"params",variants:[{begin:r.UNDERSCORE_IDENT_RE},{className:null,begin:/\(\s*\)/,skip:!0},{begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:t,contains:d.contains}]}]}],relevance:0},{className:"function",beginKeywords:"function",end:/[\{;]/,excludeEnd:!0,keywords:t,contains:["self",r.inherit(r.TITLE_MODE,{begin:"[A-Za-z$_][0-9A-Za-z$_]*"}),u],illegal:/%/,relevance:0},{beginKeywords:"constructor",end:/[\{;]/,excludeEnd:!0,contains:["self",u]},{begin:/module\./,keywords:{built_in:"module"},relevance:0},{beginKeywords:"module",end:/\{/,excludeEnd:!0},{beginKeywords:"interface",end:/\{/,excludeEnd:!0,keywords:"interface extends"},{begin:/\$[(.]/},{begin:"\\."+r.IDENT_RE,relevance:0},s,d]}}}());hljs.registerLanguage("plaintext",function(){"use strict";return function(t){return{name:"Plain text",aliases:["text","txt"],disableAutodetect:!0}}}());hljs.registerLanguage("less",function(){"use strict";return function(e){var n="([\\w-]+|@{[\\w-]+})",a=[],s=[],t=function(e){return{className:"string",begin:"~?"+e+".*?"+e}},r=function(e,n,a){return{className:e,begin:n,relevance:a}},i={begin:"\\(",end:"\\)",contains:s,relevance:0};s.push(e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,t("'"),t('"'),e.CSS_NUMBER_MODE,{begin:"(url|data-uri)\\(",starts:{className:"string",end:"[\\)\\n]",excludeEnd:!0}},r("number","#[0-9A-Fa-f]+\\b"),i,r("variable","@@?[\\w-]+",10),r("variable","@{[\\w-]+}"),r("built_in","~?`[^`]*?`"),{className:"attribute",begin:"[\\w-]+\\s*:",end:":",returnBegin:!0,excludeEnd:!0},{className:"meta",begin:"!important"});var c=s.concat({begin:"{",end:"}",contains:a}),l={beginKeywords:"when",endsWithParent:!0,contains:[{beginKeywords:"and not"}].concat(s)},o={begin:n+"\\s*:",returnBegin:!0,end:"[;}]",relevance:0,contains:[{className:"attribute",begin:n,end:":",excludeEnd:!0,starts:{endsWithParent:!0,illegal:"[<=$]",relevance:0,contains:s}}]},g={className:"keyword",begin:"@(import|media|charset|font-face|(-[a-z]+-)?keyframes|supports|document|namespace|page|viewport|host)\\b",starts:{end:"[;{}]",returnEnd:!0,contains:s,relevance:0}},d={className:"variable",variants:[{begin:"@[\\w-]+\\s*:",relevance:15},{begin:"@[\\w-]+"}],starts:{end:"[;}]",returnEnd:!0,contains:c}},b={variants:[{begin:"[\\.#:&\\[>]",end:"[;{}]"},{begin:n,end:"{"}],returnBegin:!0,returnEnd:!0,illegal:"[<='$\"]",relevance:0,contains:[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,l,r("keyword","all\\b"),r("variable","@{[\\w-]+}"),r("selector-tag",n+"%?",0),r("selector-id","#"+n),r("selector-class","\\."+n,0),r("selector-tag","&",0),{className:"selector-attr",begin:"\\[",end:"\\]"},{className:"selector-pseudo",begin:/:(:)?[a-zA-Z0-9\_\-\+\(\)"'.]+/},{begin:"\\(",end:"\\)",contains:c},{begin:"!important"}]};return a.push(e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,g,d,o,b),{name:"Less",case_insensitive:!0,illegal:"[=>'/<($\"]",contains:a}}}());hljs.registerLanguage("lua",function(){"use strict";return function(e){var t={begin:"\\[=*\\[",end:"\\]=*\\]",contains:["self"]},a=[e.COMMENT("--(?!\\[=*\\[)","$"),e.COMMENT("--\\[=*\\[","\\]=*\\]",{contains:[t],relevance:10})];return{name:"Lua",keywords:{$pattern:e.UNDERSCORE_IDENT_RE,literal:"true false nil",keyword:"and break do else elseif end for goto if in local not or repeat return then until while",built_in:"_G _ENV _VERSION __index __newindex __mode __call __metatable __tostring __len __gc __add __sub __mul __div __mod __pow __concat __unm __eq __lt __le assert collectgarbage dofile error getfenv getmetatable ipairs load loadfile loadstring module next pairs pcall print rawequal rawget rawset require select setfenv setmetatable tonumber tostring type unpack xpcall arg self coroutine resume yield status wrap create running debug getupvalue debug sethook getmetatable gethook setmetatable setlocal traceback setfenv getinfo setupvalue getlocal getregistry getfenv io lines write close flush open output type read stderr stdin input stdout popen tmpfile math log max acos huge ldexp pi cos tanh pow deg tan cosh sinh random randomseed frexp ceil floor rad abs sqrt modf asin min mod fmod log10 atan2 exp sin atan os exit setlocale date getenv difftime remove time clock tmpname rename execute package preload loadlib loaded loaders cpath config path seeall string sub upper len gfind rep find match char dump gmatch reverse byte format gsub lower table setn insert getn foreachi maxn foreach concat sort remove"},contains:a.concat([{className:"function",beginKeywords:"function",end:"\\)",contains:[e.inherit(e.TITLE_MODE,{begin:"([_a-zA-Z]\\w*\\.)*([_a-zA-Z]\\w*:)?[_a-zA-Z]\\w*"}),{className:"params",begin:"\\(",endsWithParent:!0,contains:a}].concat(a)},e.C_NUMBER_MODE,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,{className:"string",begin:"\\[=*\\[",end:"\\]=*\\]",contains:[t],relevance:5}])}}}()); diff --git a/index.html b/index.html new file mode 100644 index 0000000..e8b668f --- /dev/null +++ b/index.html @@ -0,0 +1,499 @@ + + + + + + 由 tauri 单例模式 bug “意外修复” 发现的 dangling - huangjj27's Tech blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+
+

由 tauri 单例模式 bug “意外修复” 发现的 dangling

+

TL;DR

+

tauri 单例插件 用于区分单例实例的 productName的过长会导致单例功能失效,博主最初确信 encode_wide 实现有问题,并提交了修复。然而在和社区深入研究问题原因后,发现根本原因是使用 encode_wide 转码传参时造成了 dangling

+

PS: 为方便读者理解,博主花费一天时间重新梳理分析步骤,按照演绎法展示定位 bug 地过程,实现发生的分析过程要比博文的过程更加曲折,对分析理解问题无意义因此略过。

+

所以这个 bug 的现象是怎么样的?

+

正如博主所说,在 有问题版本的插件代码 中,博主最初发现,tauri.conf.json 中的 package.productName 在分别使用五个汉字与六个汉字时,单例模式功能表现出不一致的行为:五个汉字的 productName 单例功能运行正常,而六个汉字的 productName 单例功能失效,于是针对该问题初步进行了测试:

+
+ + + +
测试的 productName单例插件功能是否生效
六个汉字试试x
随便五个字
又来了六个字x
+
+

因为这些汉字测试用例使用 UTF-8 编码,又因为是常用字,因此每个汉字对应 3 bytes,因此假设 productName 在超过 15 bytes、不超过 18 bytes 时会导致功能失效,进一步补充测试用例:

+
+ + +
测试的 productName单例插件功能是否生效
z12345678901234
z123456789012345x
+
+

看来博主运气不错,刚好踩到了边界的测试用例。那么基本可以确定,productName 超过 15 bytes 就会导致单例功能失效。

+

PotatoTooLarge: 传递给 Win32 API 的字符串要用 C string 风格的 \0 结束

+

在最初的讨论过程中,因为我们没有仔细留意插件仓库使用的 encode_wide 是自行做过封装的,因此我们一开始根据 以下代码 进行分析:

+
pub fn init<R: Runtime>(f: Box<SingleInstanceCallback<R>>) -> TauriPlugin<R> {
+    plugin::Builder::new("single-instance")
+        .setup(|app| {
+            let app_name = &app.package_info().name;
+            let class_name = format!("{}-single-instance-class", app_name);
+            let window_name = format!("{}-single-instance-window", app_name);
+
+            let hmutex = unsafe {
+                CreateMutexW(
+                    std::ptr::null(),
+                    true.into(),
+                    encode_wide("tauri-plugin-single-instance-mutex").as_ptr(),
+                )
+            };
+
+            if unsafe { GetLastError() } == ERROR_ALREADY_EXISTS {
+                unsafe {
+                    let hwnd = FindWindowW(
+                        encode_wide(&class_name).as_ptr(),
+                        encode_wide(&window_name).as_ptr(),
+                    );
+
+                    // omitted
+                }
+
+                // omitted
+            }
+
+            // omitted
+        })
+        .on_event(|app, event| {
+            // omitted
+        })
+        .build()
+}
+
+

有 Windows 编程经验的 @PotatoTooLarge 指出,encode_wide(来自 std 的 Window扩展)并不会补充 \0 结束符:

+
+

Re-encodes an OsStr as a wide character sequence, i.e., potentially ill-formed UTF-16.

+

This is lossless: calling OsStringExt::from_wide and then encode_wide on the result will yield the original code units. Note that the encoding does not add a final null terminator.

+
+

于是博主听取建议,把所有会传递到 encode_wide 函数的字符串都添加了 \0,形成了 一版修复:

+
pub fn init<R: Runtime>(f: Box<SingleInstanceCallback<R>>) -> TauriPlugin<R> {
+    plugin::Builder::new("single-instance")
+        .setup(|app| {
+            let app_name = &app.package_info().name;
+            // let class_name = format!("{}-single-instance-class", app_name);
+            let class_name = format!("{}-single-instance-class\0", app_name);
+            // let window_name = format!("{}-single-instance-window", app_name);
+            let window_name = format!("{}-single-instance-window\0", app_name);
+
+            let hmutex = unsafe {
+                CreateMutexW(
+                    std::ptr::null(),
+                    true.into(),
+                    // encode_wide("tauri-plugin-single-instance-mutex").as_ptr(),
+                    encode_wide("tauri-plugin-single-instance-mutex\0").as_ptr(),
+                )
+            };
+
+            if unsafe { GetLastError() } == ERROR_ALREADY_EXISTS {
+                unsafe {
+                    let hwnd = FindWindowW(
+                        encode_wide(&class_name).as_ptr(),
+                        encode_wide(&window_name).as_ptr(),
+                    );
+
+                    // omitted
+                }
+
+                // omitted
+            }
+
+            // omitted
+        })
+        .on_event(|app, event| {
+            // omitted
+        })
+        .build()
+}
+
+

然后使用修复前会引起单例功能失效的 z123456789012345 作为测试用例,验证单例功能可用了,证明该修改可以修复单例功能失效的问题。

+

但它并不是真的修复

+

在博主提交了修复后,插件仓库作者提醒,前文所述的代码使用的 encode_wide封装拼接了 \0 后再传递参数的:

+
pub fn encode_wide(string: impl AsRef<std::ffi::OsStr>) -> Vec<u16> {
+    std::os::windows::prelude::OsStrExt::encode_wide(string.as_ref())
+        .chain(std::iter::once(0))
+        .collect()
+}
+
+

这意味着,并不是 \0 导致问题的失效,因为该函数在 windows 环境下执行是能够补足 \0 的:

+
fn encode_wide(string: impl AsRef<std::ffi::OsStr>) -> Vec<u16> {
+    std::os::windows::prelude::OsStrExt::encode_wide(string.as_ref())
+        .chain(std::iter::once(0))
+        .collect()
+}
+
+fn main() {
+    let product_name = "z123456789012345";
+
+    // output: [122, 49, 50, 51, 52, 53, 54, 55, 56, 57, 48, 49, 50, 51, 52, 53, 0]
+    //                                                                           ^
+    //                              null concated here so it's null-terminated --|
+    println!("{:?}", encode_wide(product_name));
+}
+
+

那么失效的过程发生了什么?

+

为了分析问题详细过程,我将插件仓库代码切换到了 问题代码版本

+
git checkout 16e5e9eb59da9ceca3dcf09c81120b37fe108a03
+
+

然后添加了一些 dbg 宏:

+
pub fn init<R: Runtime>(f: Box<SingleInstanceCallback<R>>) -> TauriPlugin<R> {
+    plugin::Builder::new("single-instance")
+        .setup(|app| {
+            let app_name = &app.package_info().name;
+            let class_name = format!("{}-single-instance-class", app_name);
+            let window_name = format!("{}-single-instance-window", app_name);
+
+            let hmutex = unsafe {
+                CreateMutexW(
+                    std::ptr::null(),
+                    true.into(),
+                    encode_wide("tauri-plugin-single-instance-mutex").as_ptr(),
+                )
+            };
+            dbg!(hmutex);  // windows.rs:43 debug here!
+
+            if unsafe { GetLastError() } == ERROR_ALREADY_EXISTS {
+                unsafe {
+                    let hwnd = FindWindowW(
+                        encode_wide(&class_name).as_ptr(),
+                        encode_wide(&window_name).as_ptr(),
+                    );
+                    dbg!(hwnd);  // windows.rs:51 debug here!
+
+                    // omitted
+                }
+            } else {
+                app.manage(MutexHandle(hmutex));
+
+                let hwnd = create_event_target_window::<R>(&class_name, &window_name);
+                dbg!(hwnd);  // windows.rs:76 debug here!
+
+                // omitted
+            }
+
+            Ok(())
+        })
+        .on_event(|app, event| {
+            // omitted
+        })
+        .build()
+}
+
+

然后,将代码仓库 examples\emit-event\src-tauri\tauri.conf.json 分别改成 z12345678901234z123456789012345,然后执行:

+
# process1
+> cd examples\emit-event
+examples\emit-event> cargo tauri build --debug
+examples\emit-event> src-tauri\target\debug\z12345678901234.exe
+[tauri-plugin-single-instance\src\platform_impl\windows.rs:43] hmutex = 548
+[tauri-plugin-single-instance\src\platform_impl\windows.rs:76] hwnd = 40113446
+
+# process2
+> examples\emit-event\src-tauri\target\debug\z12345678901234.exe
+[tauri-plugin-single-instance\src\platform_impl\windows.rs:43] hmutex = 548
+[tauri-plugin-single-instance\src\platform_impl\windows.rs:51] hwnd = 40113446
+
+
# process1
+> cd examples\emit-event
+examples\emit-event> cargo tauri build --debug
+examples\emit-event> src-tauri\target\debug\z123456789012345.exe
+[tauri-plugin-single-instance\src\platform_impl\windows.rs:43] hmutex = 552
+[tauri-plugin-single-instance\src\platform_impl\windows.rs:76] hwnd = 0
+
+# process2
+> examples\emit-event\src-tauri\target\debug\z123456789012345.exe
+[tauri-plugin-single-instance\src\platform_impl\windows.rs:43] hmutex = 548
+[tauri-plugin-single-instance\src\platform_impl\windows.rs:51] hwnd = 0
+
+# process3
+> examples\emit-event\src-tauri\target\debug\z123456789012345.exe
+[tauri-plugin-single-instance\src\platform_impl\windows.rs:43] hmutex = 548
+[tauri-plugin-single-instance\src\platform_impl\windows.rs:51] hwnd = 0
+
+

由上面的结果我们可以知道:在用例 z12345678901234,我们在创建实实例时返回了有效的 hwnd 值,并且在检查 hwnd 时确认已经创建窗口;而在用例 z123456789012345,我们创建窗口的函数 create_event_target_window 返回的 hwnd 是无效的!所以导致问题的代码,应该在 create_event_target_window 的逻辑中!再次添加 dbg ,重新编译后继续 debug:

+
fn create_event_target_window<R: Runtime>(class_name: &str, window_name: &str) -> HWND {
+    unsafe {
+        let class = WNDCLASSEXW {
+            cbSize: std::mem::size_of::<WNDCLASSEXW>() as u32,
+            style: 0,
+            lpfnWndProc: Some(single_instance_window_proc::<R>),
+            cbClsExtra: 0,
+            cbWndExtra: 0,
+            hInstance: GetModuleHandleW(std::ptr::null()),
+            hIcon: 0,
+            hCursor: 0,
+            hbrBackground: 0,
+            lpszMenuName: std::ptr::null(),
+            lpszClassName: encode_wide(&class_name).as_ptr(),
+            hIconSm: 0,
+        };
+        dbg!(class.lpszClassName);  // windows.rs:153 debug here
+        dbg!(*class.lpszClassName);  // windows.rs:154 debug here
+
+        RegisterClassExW(&class);
+
+        let hwnd = CreateWindowExW(
+            WS_EX_NOACTIVATE
+            | WS_EX_TRANSPARENT
+            | WS_EX_LAYERED
+            // WS_EX_TOOLWINDOW prevents this window from ever showing up in the taskbar, which
+            // we want to avoid. If you remove this style, this window won't show up in the
+            // taskbar *initially*, but it can show up at some later point. This can sometimes
+            // happen on its own after several hours have passed, although this has proven
+            // difficult to reproduce. Alternatively, it can be manually triggered by killing
+            // `explorer.exe` and then starting the process back up.
+            // It is unclear why the bug is triggered by waiting for several hours.
+            | WS_EX_TOOLWINDOW,
+            dbg!(encode_wide(&class_name).as_ptr()),  // windows.rs:170 debug here
+            dbg!(encode_wide(&window_name).as_ptr()),  // windows.rs:171 debug here
+            WS_OVERLAPPED,
+            0,
+            0,
+            0,
+            0,
+            0,
+            0,
+            GetModuleHandleW(std::ptr::null()),
+            std::ptr::null(),
+        );
+        SetWindowLongPtrW(
+            hwnd,
+            GWL_STYLE,
+            // The window technically has to be visible to receive WM_PAINT messages (which are used
+            // for delivering events during resizes), but it isn't displayed to the user because of
+            // the LAYERED style.
+            (WS_VISIBLE | WS_POPUP) as isize,
+        );
+        hwnd
+    }
+}
+
+

z12345678901234:

+
examples\emit-event> src-tauri\target\debug\z12345678901234.exe
+[tauri-plugin-single-instance\src\platform_impl\windows.rs:43] hmutex = 556
+[tauri-plugin-single-instance\src\platform_impl\windows.rs:153] class.lpszClassName = 0x0000021d099eddc0
+[tauri-plugin-single-instance\src\platform_impl\windows.rs:154] *class.lpszClassName = 122
+[tauri-plugin-single-instance\src\platform_impl\windows.rs:170] encode_wide(&class_name).as_ptr() = 0x0000021d099ee180
+[tauri-plugin-single-instance\src\platform_impl\windows.rs:171] encode_wide(&window_name).as_ptr() = 0x0000021d099dfe20
+[tauri-plugin-single-instance\src\platform_impl\windows.rs:76] hwnd = 28841288
+
+

z123456789012345:

+
examples\emit-event> src-tauri\target\debug\z123456789012345.exe
+[tauri-plugin-single-instance\src\platform_impl\windows.rs:43] hmutex = 548
+[tauri-plugin-single-instance\src\platform_impl\windows.rs:153] class.lpszClassName = 0x0000017259ca6be0
+[tauri-plugin-single-instance\src\platform_impl\windows.rs:154] *class.lpszClassName = 43920
+[tauri-plugin-single-instance\src\platform_impl\windows.rs:170] encode_wide(&class_name).as_ptr() = 0x0000017259cc6b30
+[tauri-plugin-single-instance\src\platform_impl\windows.rs:171] encode_wide(&window_name).as_ptr() = 0x0000017259cc6970
+[tauri-plugin-single-instance\src\platform_impl\windows.rs:76] hwnd = 0
+
+

对比我们之前 encode_wide 函数返回的结果class_name 开头的字符应该是 ASCII 字符 z(ASCII 码 122),因此通过 encode_wide(&class_name).as_ptr() 传参的 class.lpszClassName,应当指向值为 "z123456789012345-single-instance-class" 的字符串,这在用例 z12345678901234 中行为符合预期;但在 z123456789012345 的用例中,class.lpszClassName 指向的却发生了变化(*class.lpszClassName = 43920),反推可以得知, encode_wide(&class_name).as_ptr() 并没有成功地把指针传递给 class.lpszClassName

+

一语惊醒梦中人:悬垂指针(dangling)!

+

@Berrysoft 指出, encode_wide(&class_name).as_ptr() 这种写法由于直接对临时变量直接取指针,而临时变量 encode_wide(&class_name) 会在执行完之后被马上释放结束生命周期,因此指向该临时变量的指针也会变成悬垂指针!临时变量的这一行为在 reference 中有说明:

+
+

When using a value expression in most place expression contexts, a temporary unnamed memory location is created and initialized to that value. The expression evaluates to that location instead, except if promoted to a static. The drop scope of the temporary is usually the end of the enclosing statement.

+
+

而解决该问题,只需要把提升变量的 lifetime,把要用到的变量提取出来,使其 lifetime 可以覆盖要用到的函数而不至于在语句执行完之后马上被回收。于是有了解决问题的 PR

+

那为什么在 format 的时候手动添加 \0 后,问题“修复”了呢?

+

这个问题依然悬而未决。有 TG 群友提出,可能是由于堆栈被破坏 “碰巧” 又指向了正确的字符串位置,而 format 后的变量又是 'static 的,因此能达到“修复”的效果,然而这依然是基于 bug/undefined behavior 的修复方案,因此仍然不可靠。后续原因排查出来后会更新博客~

+

教训与经验

+
    +
  1. 实际上的排查过程,是分析过一次 create_event_target_window 的,然而当时由于需求紧急而找到了临时绕开的实现方案(把 productName 砍短),因此搁置了,也没有留下相关的排查记录文档,以致于后续在需求变更而变得必须排查清楚该问题时,走向了排查 encode_wide 的错误方向,虽然有了“修复”方案,但该方案仍然不可靠,因此可以视作浪费了实践。 形成记录首先方便的是以后的自己。
  2. +
  3. 凡是 unsafe 多查几遍。像本文涉及到的悬垂指针问题,在 safe rust 中因为 lifetime 不够长而会阻止编译,而 unsafe 块中使用裸指针是不会被编译器检查的,因此相关操作都要相当慎重。
  4. +
  5. 多借助社区的力量。比起一个人钻牛角尖,多与社区讨论才容易跳出原本的死胡同,从而理解意识到原来思路的局限性。
  6. +
+ +
+ +
+ + + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + diff --git a/interview.html b/interview.html new file mode 100644 index 0000000..a46530e --- /dev/null +++ b/interview.html @@ -0,0 +1,554 @@ + + + + + + 记一次面试 - huangjj27's Tech blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+
+

总结一次面试

+

最值得总结的三个问题

+

线程同步有哪些方法?如何用这些方法实现一个 RwLock?

+

线程同步的目的在于解决多个线程访问关键资源时的竞争状态。一个数据竞争的简单例子如下:

+
use std::thread;
+fn main() {
+  let mut s = String::from("Hello");
+
+  let thread1 = thread::spawn(|| {
+    println!("{}", s);
+  });
+
+  let thread2 = thread::spawn(|| {
+    s.push_str("World!");
+    println!("{}", s);
+  });
+
+  thread1.join();
+  thread2.join();
+}
+
+

上文的代码中 thread1 试图打印 s, 预期得到输出 Hello, 但是 thread2 却改变了 s 的内容, +那么 thread1 最终打印内容将取决于两个线程哪个先完成: 如果 thread1 先完成了,那么 +将打印 Hello; 如果 thread2 先完成了,那么将打印 HelloWorld!

+

实际上得益于 Rust 的所有权系统与生命周期(lifetime)检查,上述示例并不能编译——子线程可能 +会在主程序结束后继续运行,导致子线程捕获的 s 的引用失效;另外 thread2 直接修改了 s, +换言之只会允许 thread2 独占地持有 s 的可变引用(&mut s),而不允许其他线程持有 s +的任何引用。

+

在 Rust 编程中,主要有以下线程同步的方法:

+
    +
  • 互斥锁(Mutex) +我们可以使用互斥锁 Mutex<T> 来控制只能有单独一个线程读取/修改 +对象。通常实践是在外面加上原子引用计数 Arc 变成 Arc<Mutex<T>>,来减少 Mutex +拷贝的开销。对于多读少写的场景,可以用 RwLock 提高并发。
  • +
  • 条件变量(CondVar) +条件变量用于“阻塞”线程并使得线程在等待事件时不需要消耗 CPU 时间。通常会与放进互斥锁 +布尔型的预言值(状态值)关联使用,在状态值发生变化时通知条件变量。
  • +
  • 屏障(Barrier) +屏障用于在某个需要若干线程 都完成 前置操作后再开始计算的操作之前,让所有所需线程 +的状态都达到能开始进行计算的状态。
  • +
+

有什么问题是生存期标注无法修正的?请给出一个例子

+

这道问题最后我也并没有理解,“生命周期标注无法修正的问题”,字面意思是,即使我们按照我们 +期望的程序语义来修正了生命周期标注,这个程序仍然不能通过编译,或者再运行时仍然不能得到 +期望结果。按此描述,一个可能的例子是,我们尝试从一个较短的引用返回一个较长的引用:

+
fn longhten<'a>(s_ref: &'a str) -> &'static str {
+    s_ref
+}
+
+fn main() {
+    let s = String::from("hello");
+
+    let static_ref = longhten(&s);
+
+    println!("{}", static_ref);
+}
+
+
+

Waker 如何被唤醒? Reactor要怎么实现?

+

Reactor 作为反应器,上面同时挂载了成千上万个待唤醒的事件,这里使用了mio统一封装了操作系统的多路复用API。 +在Linux中使用的是Epoll1,在Mac中使用的则是Kqueue2

+
loop {
+    // 轮询事件是否超时
+    poll.poll(&events, timeout);
+    for event in events.iter() {
+        if (event.is_readable()) {
+            for waker in event.readers.wakers {
+                waker.wake();
+            }
+        }
+        if (event.is_writeable()) {
+            for waker in event.writers.wakers {
+                waker.wake();
+            }
+        }
+    }
+}
+
+

一面 -- 手写代码

+

1. 实现一个二分查找函数

+
#![allow(unused)]
+fn main() {
+use std::cmp::Ordering;
+
+/// 给出一个从小到大排列的数组,请实现一个函数,用二分法把指定数组 x 的位置找出来。若 x
+/// 不存在,则返回 -1. 若 x 存在多个,请返回 x 在数组中第一次出现的位置
+fn find(arr: Vec<i32>, x: i32) -> i32 {
+    let (mut left, mut right) = (0, arr.len() - 1);
+    loop {
+        if left > right {
+            return -1;
+        }
+
+        let mut mid = (left + right) / 2;
+        match arr[mid].cmp(&x) {
+            // 记得要排除已经命中的元素!
+            Ordering::Less => left = mid + 1,
+            Ordering::Greater => right = mid - 1,
+            Ordering::Equal => {
+                while mid >= 1 && arr[mid - 1] == x {
+                    mid -= 1;
+                }
+
+                return mid as i32;
+            },
+        }
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use super::*;
+
+    #[test]
+    fn should_return_minus1() {
+        let arr = vec![1, 3, 5];
+        let x = 2;
+
+        assert_eq!(find(arr, x), -1);
+    }
+
+    #[test]
+    fn should_return_mid() {
+        let arr = vec![1, 3, 5, 7, 9, 10, 10];
+        let x = 7;
+
+        assert_eq!(find(arr, x), 3);
+    }
+
+    #[test]
+    fn should_return_first() {
+        let arr = vec![1, 3, 5, 7, 9, 10, 10];
+        let x = 10;
+
+        assert_eq!(find(arr, x), 5);
+    }
+}
+}
+
+
    +
  • +

    Q: 请分析该函数的算法复杂度?

    +
      +
    • A: 时间复杂度 \( O(\log n) \),最坏情况下的事件复杂度是 \( O(n) \)
    • +
    +
  • +
  • +

    Q: 请优化这个算法?

    +
      +
    • A:一个优化方法是 插值查找法,利用如下公式自动根据查找到的元素与目标的距离来修正下一次查找 +的区间范围,提高查找速度:
    • +
    +
  • +
+

\[ mid = left + { key - arr[left] \over arr[right] - key } (right - left) \]

+

2. 镜像二叉树

+

请反转二叉树。如给出以下二叉树:

+
     1
+   /   \
+  2     3
+ / \   / \
+4   5 6   7
+
+

反转为:

+
     1
+   /   \
+  3     2
+ / \   / \
+7   6 5   4
+
+

递归解法:

+
#![allow(unused)]
+fn main() {
+use std::convert::Into;
+
+struct Node {
+    val: i32,
+    left: NodeLink,
+    right: NodeLink,
+}
+
+type NodeLink = Option<Box<Node>>;
+
+fn construct_tree() -> Box<Node> {
+    let l3left1 = Node {
+        val: 4,
+        left: None,
+        right: None,
+    };
+
+    let l3right1 = Node {
+        val: 5,
+        left: None,
+        right: None,
+    };
+
+    let l3left2 = Node {
+        val: 6,
+        left: None,
+        right: None,
+    };
+
+    let l3right2 = Node {
+        val: 7,
+        left: None,
+        right: None,
+    };
+
+    let l2left = Node {
+        val: 2,
+        left: Some(Box::new(l3left1)),
+        right: Some(Box::new(l3right1)),
+    };
+
+
+    let l2right = Node {
+        val: 3,
+        left: Some(Box::new(l3left2)),
+        right: Some(Box::new(l3right2)),
+    };
+
+    Box::new(Node {
+        val: 1,
+        left: Some(Box::new(l2left)),
+        right: Some(Box::new(l2right)),
+    })
+}
+
+fn construct_mirror() -> Box<Node> {
+    let l3left1 = Node {
+        val: 7,
+        left: None,
+        right: None,
+    };
+
+    let l3right1 = Node {
+        val: 6,
+        left: None,
+        right: None,
+    };
+
+    let l3left2 = Node {
+        val: 5,
+        left: None,
+        right: None,
+    };
+
+    let l3right2 = Node {
+        val: 4,
+        left: None,
+        right: None,
+    };
+
+    let l2left = Node {
+        val: 3,
+        left: Some(Box::new(l3left1)),
+        right: Some(Box::new(l3right1)),
+    };
+
+
+    let l2right = Node {
+        val: 2,
+        left: Some(Box::new(l3left2)),
+        right: Some(Box::new(l3right2)),
+    };
+
+    Box::new(Node {
+        val: 1,
+        left: Some(Box::new(l2left)),
+        right: Some(Box::new(l2right)),
+    })
+}
+
+impl Into<Vec<i32>> for Box<Node> {
+    fn into(mut self) -> Vec<i32> {
+        let v_left: Vec<i32>;
+        let v_right: Vec<i32>;
+        v_left = if let Some(node) = self.left.take() {
+            node.into()
+        } else {
+            Vec::new()
+        };
+
+        v_right = if let Some(node) = self.right.take() {
+            node.into()
+        } else {
+            Vec::new()
+        };
+
+       let mut v = Vec::new();
+       v.push(self.val);
+       v.extend(v_left.into_iter());
+       v.extend(v_right.into_iter());
+
+       v
+    }
+}
+
+fn mirror(root: &mut Node) {
+    let (mut tmp_left, mut tmp_right) = (NodeLink::None, NodeLink::None);
+
+    if let Some(mut node) = root.left.take() {
+        mirror(&mut node);
+        tmp_left = Some(node);
+    }
+
+    if let Some(mut node) = root.right.take() {
+        mirror(&mut node);
+        tmp_right = Some(node);
+    }
+
+    root.left = tmp_right;
+    root.right = tmp_left;
+}
+
+#[cfg(test)]
+mod test {
+    use super::*;
+
+    #[test]
+    fn should_mirrored() {
+        let mut tree = construct_tree();
+        let expect = construct_mirror();
+
+        let mirror = mirror(&mut tree);
+        assert_eq!(<Box<Node> as Into<Vec<i32>>>::into(tree), <Box<Node> as Into<Vec<i32>>>::into(expect));
+    }
+}
+}
+
+
    +
  • Q: 请优化这个算法? +
      +
    • A:如果不用递归(因为递归会加深调用栈),可以使用 广度优先搜索算法 来自根向叶 +逐层反转左右子节点的指针,并将子节点的指针放入到队列中待进行处理。
    • +
    +
  • +
+
+ + + +
+ +
+ + + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + diff --git a/load-wasm-mistake.html b/load-wasm-mistake.html new file mode 100644 index 0000000..7d55c01 --- /dev/null +++ b/load-wasm-mistake.html @@ -0,0 +1,516 @@ + + + + + + 尝试在单 HTML 文件中嵌入 WASM 模块的错误操作 - huangjj27's Tech blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+
+

尝试在单 HTML 文件中嵌入 WASM 模块的错误操作

+

TL;DR

+
    +
  1. 当项目是需要 WASM 与 JavaScript 相互交互的时候,请尽可能在统一的 JavaScript 入口中定义所有的功能;
  2. +
  3. wasm-pack 生成的胶水 JavaScript 与 WASM 可以稍作修改即可嵌入到 HTML 文件中。
  4. +
+

项目背景

+

当博主兴高采烈地使用 HTML 与 JavaScript 迅速开发好 UI 界面与交互功能的时候,发现核心的功能的 JavaScript 库只支持 npm 环境而无法应用到前述 UI 界面上,博主迫于无奈只能抓起以前做过的 Rust 版本库,尝试改造成 WASM 模块以复用界面代码。因为博主的这个项目是属于不对外开放的项目,因此本文中使用的项目是简化后的 demo,但不影响博主记录以及提醒上述遇到的两个问题(这两个坑每一个都坑掉了我几个小时,但愿会有读者看到我这篇文章抢救一下自己的时间)。

+

demo 项目架构

+
demo
+  |-- Cargo.toml
+  |-- src
+        |-- lib.rs
+  |-- assets
+        |-- demo.html
+
+
# Cargo.toml
+[package]
+name = "demo"
+authors = ["huangjj27 <huangjj.27@qq.com>"]
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[lib]
+crate-type = ["cdylib", "rlib"]
+
+[dependencies]
+wasm-bindgen = "0.2.63"
+
+
// lib.rs
+use wasm_bindgen::prelude::*;
+
+#[wasm_bindgen]
+pub fn add(a: i32, b: i32) -> i32 {
+    a + b
+}
+
+
<!-- demo.html -->
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>just a demo</title>
+    <script id="indexscript" type="module">
+        import init, { add } from "../pkg/demo.js";
+        async function run() {
+            await init();
+        }
+
+        run();
+    </script>
+    <script>
+        function js_add() {
+            let a = document.getElementById("a");
+            let b = document.getElementById("b");
+            let sum = document.getElementById("sum");
+            sum.value = add(a.value, b.value);
+        }
+    </script>
+</head>
+<body>
+    <textarea id="a" autofocus oninput="js_add()">40</textarea>
+    <textarea id="b" oninput="js_add()">2</textarea>
+    <textarea id="sum" readonly></textarea>
+</body>
+</html>
+
+

项目在编译的时候还需要 wasm-pack,用来生成胶水 JavaScript demo/pkg/demo.js 和 wasm 文件 demo/pkg/demo_bg.wasm

+
wasm-pack build --target=web
+
+

薛定谔的 JavaScript 函数

+

当我们打开 demo.html 并且尝试修改 ab 的值时,我们会从控制台遇到了如下报错:

+
13:33:19.672 Uncaught ReferenceError: add is not defined
+    js_add file:///demo/assets/demo.html:23
+    oninput file:///demo/assets/demo.html:1
+2 demo.html:22:13
+
+

这里的 add 函数就是我们从 WASM 模块中加载的,来自 rust 实现的 add 函数。此时,如果我们在 run 下属下方,直接调用 add 则是可以执行成功的:

+
    <script id="indexscript" type="module">
+        import init, { add } from "../pkg/demo.js";
+        async function run() {
+            await init();
+            console.log(`add函数已加载,add(1, 2) = ${add(1, 2)}`);
+        }
+
+        run();
+    </script>
+
+

执行结果:

+
14:02:58.360 add函数已加载,add(1, 2) = 3 demo.html:13:21
+14:03:11.895 Uncaught ReferenceError: add is not defined
+    js_add file:///demo/assets/demo.html:23
+    oninput file:///demo/assets/demo.html:1
+2 demo.html:23:13
+
+

出现以上现象的原因是,type="module" 限制了 indexscript 内部项目的作用域只能在该 <script> 代码块中有效。解决方法有两种:

+
    +
  1. 将需要导出的给其他 <script> 块使用的功能,挂载在页面全局的 window 对象上,模拟 export 的效果,缺点是很可能无意中覆盖了挂载对象。
  2. +
  3. 将所有js 功能都集中在 indexscript 代码块中,将所有的功能统一管理。此时,要注意将DOM 元素的事件通过监听事件的方式来管理:
  4. +
+
<!-- demo.html -->
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>just a demo</title>
+    <script id="indexscript" type="module">
+        import init, { add } from "../pkg/demo.js";
+        function js_add() {
+            let a = document.getElementById("a");
+            let b = document.getElementById("b");
+            let sum = document.getElementById("sum");
+            sum.value = add(a.value, b.value);
+        }
+
+        async function run() {
+            await init();
+
+            document.getElementById("a").addEventListener("input", js_add);
+            document.getElementById("b").addEventListener("input", js_add);
+        }
+
+        run();
+    </script>
+</head>
+<body>
+    <textarea id="a" autofocus>40</textarea>
+    <textarea id="b">2</textarea>
+    <textarea id="sum" readonly></textarea>
+</body>
+</html>
+
+

然而我还是想包容你的,我的 WASM

+

我们来分析一下 wasm-pack 生成的 demo.js

+
// demo.js
+
+let wasm;
+
+/**
+* @param {number} a
+* @param {number} b
+* @returns {number}
+*/
+export function add(a, b) {
+    const ret = wasm.add(a, b);
+    return ret;
+}
+
+async function load(module, imports) {
+    if (typeof Response === 'function' && module instanceof Response) {
+        if (typeof WebAssembly.instantiateStreaming === 'function') {
+            try {
+                return await WebAssembly.instantiateStreaming(module, imports);
+
+            } catch (e) {
+                if (module.headers.get('Content-Type') != 'application/wasm') {
+                    console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e);
+
+                } else {
+                    throw e;
+                }
+            }
+        }
+
+        const bytes = await module.arrayBuffer();
+        return await WebAssembly.instantiate(bytes, imports);
+
+    } else {
+        const instance = await WebAssembly.instantiate(module, imports);
+
+        if (instance instanceof WebAssembly.Instance) {
+            return { instance, module };
+
+        } else {
+            return instance;
+        }
+    }
+}
+
+async function init(input) {
+    if (typeof input === 'undefined') {
+        input = new URL('demo_bg.wasm', import.meta.url);
+    }
+    const imports = {};
+
+
+    if (typeof input === 'string' || (typeof Request === 'function' && input instanceof Request) || (typeof URL === 'function' && input instanceof URL)) {
+        input = fetch(input);
+    }
+
+
+
+    const { instance, module } = await load(await input, imports);
+
+    wasm = instance.exports;
+    init.__wbindgen_wasm_module = module;
+
+    return wasm;
+}
+
+export default init;
+
+

我们看到,demo.js:175 对来待加载的 module 做了判断:如果不是从响应获取的数据,则直接视作 wasm bytes 来进行加载。于是我们可以对生成的 demo_bg.wasm 文件通过 base64 转码,嵌入到 HTML 文件中,然后转换成 Uint8Array 传递给 demo.js:init 函数进行加载:

+
    <script id="indexscript" type="module">
+// demo.js 完全嵌入到 html 文件中,并且不需要 export 语句
+let wasm;
+
+/**
+* @param {number} a
+* @param {number} b
+* @returns {number}
+*/
+function add(a, b) {
+    const ret = wasm.add(a, b);
+    return ret;
+}
+
+async function load(module, imports) {
+    if (typeof Response === 'function' && module instanceof Response) {
+        if (typeof WebAssembly.instantiateStreaming === 'function') {
+            try {
+                return await WebAssembly.instantiateStreaming(module, imports);
+
+            } catch (e) {
+                if (module.headers.get('Content-Type') != 'application/wasm') {
+                    console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e);
+
+                } else {
+                    throw e;
+                }
+            }
+        }
+
+        const bytes = await module.arrayBuffer();
+        return await WebAssembly.instantiate(bytes, imports);
+
+    } else {
+        const instance = await WebAssembly.instantiate(module, imports);
+
+        if (instance instanceof WebAssembly.Instance) {
+            return { instance, module };
+
+        } else {
+            return instance;
+        }
+    }
+}
+
+async function init(input) {
+    if (typeof input === 'undefined') {
+        input = new URL('demo_bg.wasm', import.meta.url);
+    }
+    const imports = {};
+
+
+    if (typeof input === 'string' || (typeof Request === 'function' && input instanceof Request) || (typeof URL === 'function' && input instanceof URL)) {
+        input = fetch(input);
+    }
+
+
+
+    const { instance, module } = await load(await input, imports);
+
+    wasm = instance.exports;
+    init.__wbindgen_wasm_module = module;
+
+    return wasm;
+}
+
+// demo.js 嵌入结束
+
+        // wasm 二进制,base64编码:
+        const WASM_B64 = "AGFzbQEAAAABBwFgAn9/AX8DAgEABQMBABEHEAIGbWVtb3J5AgADYWRkAAAKCQEHACAAIAFqCwB7CXByb2R1Y2VycwIIbGFuZ3VhZ2UBBFJ1c3QADHByb2Nlc3NlZC1ieQMFcnVzdGMdMS42MS4wIChmZTViMTNkNjggMjAyMi0wNS0xOCkGd2FscnVzBjAuMTkuMAx3YXNtLWJpbmRnZW4SMC4yLjgwICg0Y2FhOTgxNjUp";
+
+        function b64toBytes(b64) {
+            let binary = atob(b64);
+            let bytes = new Uint8Array(binary.length);
+            for (let i = 0; i < bytes.length; i++) {
+                bytes[i] = binary.charCodeAt(i);
+            }
+            return bytes;
+        }
+
+        function js_add() {
+            let a = document.getElementById("a");
+            let b = document.getElementById("b");
+            let sum = document.getElementById("sum");
+            sum.value = add(a.value, b.value);
+        }
+
+        async function run() {
+            await init(b64toBytes(WASM_B64));  // 直接通过页面加载编码
+
+            document.getElementById("a").addEventListener("input", js_add);
+            document.getElementById("b").addEventListener("input", js_add);
+        }
+
+        run();
+    </script>
+
+

完成以上步骤,我们就得到了一个带有 WASM 功能的 standalone html 文件啦~。

+ +
+ +
+ + + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + diff --git a/mark.min.js b/mark.min.js new file mode 100644 index 0000000..1636231 --- /dev/null +++ b/mark.min.js @@ -0,0 +1,7 @@ +/*!*************************************************** +* mark.js v8.11.1 +* https://markjs.io/ +* Copyright (c) 2014–2018, Julian Kühnel +* Released under the MIT license https://git.io/vwTVl +*****************************************************/ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):e.Mark=t()}(this,function(){"use strict";var e="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},t=function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")},n=function(){function e(e,t){for(var n=0;n1&&void 0!==arguments[1])||arguments[1],i=arguments.length>2&&void 0!==arguments[2]?arguments[2]:[],o=arguments.length>3&&void 0!==arguments[3]?arguments[3]:5e3;t(this,e),this.ctx=n,this.iframes=r,this.exclude=i,this.iframesTimeout=o}return n(e,[{key:"getContexts",value:function(){var e=[];return(void 0!==this.ctx&&this.ctx?NodeList.prototype.isPrototypeOf(this.ctx)?Array.prototype.slice.call(this.ctx):Array.isArray(this.ctx)?this.ctx:"string"==typeof this.ctx?Array.prototype.slice.call(document.querySelectorAll(this.ctx)):[this.ctx]:[]).forEach(function(t){var n=e.filter(function(e){return e.contains(t)}).length>0;-1!==e.indexOf(t)||n||e.push(t)}),e}},{key:"getIframeContents",value:function(e,t){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:function(){},r=void 0;try{var i=e.contentWindow;if(r=i.document,!i||!r)throw new Error("iframe inaccessible")}catch(e){n()}r&&t(r)}},{key:"isIframeBlank",value:function(e){var t="about:blank",n=e.getAttribute("src").trim();return e.contentWindow.location.href===t&&n!==t&&n}},{key:"observeIframeLoad",value:function(e,t,n){var r=this,i=!1,o=null,a=function a(){if(!i){i=!0,clearTimeout(o);try{r.isIframeBlank(e)||(e.removeEventListener("load",a),r.getIframeContents(e,t,n))}catch(e){n()}}};e.addEventListener("load",a),o=setTimeout(a,this.iframesTimeout)}},{key:"onIframeReady",value:function(e,t,n){try{"complete"===e.contentWindow.document.readyState?this.isIframeBlank(e)?this.observeIframeLoad(e,t,n):this.getIframeContents(e,t,n):this.observeIframeLoad(e,t,n)}catch(e){n()}}},{key:"waitForIframes",value:function(e,t){var n=this,r=0;this.forEachIframe(e,function(){return!0},function(e){r++,n.waitForIframes(e.querySelector("html"),function(){--r||t()})},function(e){e||t()})}},{key:"forEachIframe",value:function(t,n,r){var i=this,o=arguments.length>3&&void 0!==arguments[3]?arguments[3]:function(){},a=t.querySelectorAll("iframe"),s=a.length,c=0;a=Array.prototype.slice.call(a);var u=function(){--s<=0&&o(c)};s||u(),a.forEach(function(t){e.matches(t,i.exclude)?u():i.onIframeReady(t,function(e){n(t)&&(c++,r(e)),u()},u)})}},{key:"createIterator",value:function(e,t,n){return document.createNodeIterator(e,t,n,!1)}},{key:"createInstanceOnIframe",value:function(t){return new e(t.querySelector("html"),this.iframes)}},{key:"compareNodeIframe",value:function(e,t,n){if(e.compareDocumentPosition(n)&Node.DOCUMENT_POSITION_PRECEDING){if(null===t)return!0;if(t.compareDocumentPosition(n)&Node.DOCUMENT_POSITION_FOLLOWING)return!0}return!1}},{key:"getIteratorNode",value:function(e){var t=e.previousNode();return{prevNode:t,node:null===t?e.nextNode():e.nextNode()&&e.nextNode()}}},{key:"checkIframeFilter",value:function(e,t,n,r){var i=!1,o=!1;return r.forEach(function(e,t){e.val===n&&(i=t,o=e.handled)}),this.compareNodeIframe(e,t,n)?(!1!==i||o?!1===i||o||(r[i].handled=!0):r.push({val:n,handled:!0}),!0):(!1===i&&r.push({val:n,handled:!1}),!1)}},{key:"handleOpenIframes",value:function(e,t,n,r){var i=this;e.forEach(function(e){e.handled||i.getIframeContents(e.val,function(e){i.createInstanceOnIframe(e).forEachNode(t,n,r)})})}},{key:"iterateThroughNodes",value:function(e,t,n,r,i){for(var o,a=this,s=this.createIterator(t,e,r),c=[],u=[],l=void 0,h=void 0;void 0,o=a.getIteratorNode(s),h=o.prevNode,l=o.node;)this.iframes&&this.forEachIframe(t,function(e){return a.checkIframeFilter(l,h,e,c)},function(t){a.createInstanceOnIframe(t).forEachNode(e,function(e){return u.push(e)},r)}),u.push(l);u.forEach(function(e){n(e)}),this.iframes&&this.handleOpenIframes(c,e,n,r),i()}},{key:"forEachNode",value:function(e,t,n){var r=this,i=arguments.length>3&&void 0!==arguments[3]?arguments[3]:function(){},o=this.getContexts(),a=o.length;a||i(),o.forEach(function(o){var s=function(){r.iterateThroughNodes(e,o,t,n,function(){--a<=0&&i()})};r.iframes?r.waitForIframes(o,s):s()})}}],[{key:"matches",value:function(e,t){var n="string"==typeof t?[t]:t,r=e.matches||e.matchesSelector||e.msMatchesSelector||e.mozMatchesSelector||e.oMatchesSelector||e.webkitMatchesSelector;if(r){var i=!1;return n.every(function(t){return!r.call(e,t)||(i=!0,!1)}),i}return!1}}]),e}(),o=function(){function e(n){t(this,e),this.opt=r({},{diacritics:!0,synonyms:{},accuracy:"partially",caseSensitive:!1,ignoreJoiners:!1,ignorePunctuation:[],wildcards:"disabled"},n)}return n(e,[{key:"create",value:function(e){return"disabled"!==this.opt.wildcards&&(e=this.setupWildcardsRegExp(e)),e=this.escapeStr(e),Object.keys(this.opt.synonyms).length&&(e=this.createSynonymsRegExp(e)),(this.opt.ignoreJoiners||this.opt.ignorePunctuation.length)&&(e=this.setupIgnoreJoinersRegExp(e)),this.opt.diacritics&&(e=this.createDiacriticsRegExp(e)),e=this.createMergedBlanksRegExp(e),(this.opt.ignoreJoiners||this.opt.ignorePunctuation.length)&&(e=this.createJoinersRegExp(e)),"disabled"!==this.opt.wildcards&&(e=this.createWildcardsRegExp(e)),e=this.createAccuracyRegExp(e),new RegExp(e,"gm"+(this.opt.caseSensitive?"":"i"))}},{key:"escapeStr",value:function(e){return e.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g,"\\$&")}},{key:"createSynonymsRegExp",value:function(e){var t=this.opt.synonyms,n=this.opt.caseSensitive?"":"i",r=this.opt.ignoreJoiners||this.opt.ignorePunctuation.length?"\0":"";for(var i in t)if(t.hasOwnProperty(i)){var o=t[i],a="disabled"!==this.opt.wildcards?this.setupWildcardsRegExp(i):this.escapeStr(i),s="disabled"!==this.opt.wildcards?this.setupWildcardsRegExp(o):this.escapeStr(o);""!==a&&""!==s&&(e=e.replace(new RegExp("("+this.escapeStr(a)+"|"+this.escapeStr(s)+")","gm"+n),r+"("+this.processSynonyms(a)+"|"+this.processSynonyms(s)+")"+r))}return e}},{key:"processSynonyms",value:function(e){return(this.opt.ignoreJoiners||this.opt.ignorePunctuation.length)&&(e=this.setupIgnoreJoinersRegExp(e)),e}},{key:"setupWildcardsRegExp",value:function(e){return(e=e.replace(/(?:\\)*\?/g,function(e){return"\\"===e.charAt(0)?"?":""})).replace(/(?:\\)*\*/g,function(e){return"\\"===e.charAt(0)?"*":""})}},{key:"createWildcardsRegExp",value:function(e){var t="withSpaces"===this.opt.wildcards;return e.replace(/\u0001/g,t?"[\\S\\s]?":"\\S?").replace(/\u0002/g,t?"[\\S\\s]*?":"\\S*")}},{key:"setupIgnoreJoinersRegExp",value:function(e){return e.replace(/[^(|)\\]/g,function(e,t,n){var r=n.charAt(t+1);return/[(|)\\]/.test(r)||""===r?e:e+"\0"})}},{key:"createJoinersRegExp",value:function(e){var t=[],n=this.opt.ignorePunctuation;return Array.isArray(n)&&n.length&&t.push(this.escapeStr(n.join(""))),this.opt.ignoreJoiners&&t.push("\\u00ad\\u200b\\u200c\\u200d"),t.length?e.split(/\u0000+/).join("["+t.join("")+"]*"):e}},{key:"createDiacriticsRegExp",value:function(e){var t=this.opt.caseSensitive?"":"i",n=this.opt.caseSensitive?["aàáảãạăằắẳẵặâầấẩẫậäåāą","AÀÁẢÃẠĂẰẮẲẴẶÂẦẤẨẪẬÄÅĀĄ","cçćč","CÇĆČ","dđď","DĐĎ","eèéẻẽẹêềếểễệëěēę","EÈÉẺẼẸÊỀẾỂỄỆËĚĒĘ","iìíỉĩịîïī","IÌÍỈĨỊÎÏĪ","lł","LŁ","nñňń","NÑŇŃ","oòóỏõọôồốổỗộơởỡớờợöøō","OÒÓỎÕỌÔỒỐỔỖỘƠỞỠỚỜỢÖØŌ","rř","RŘ","sšśșş","SŠŚȘŞ","tťțţ","TŤȚŢ","uùúủũụưừứửữựûüůū","UÙÚỦŨỤƯỪỨỬỮỰÛÜŮŪ","yýỳỷỹỵÿ","YÝỲỶỸỴŸ","zžżź","ZŽŻŹ"]:["aàáảãạăằắẳẵặâầấẩẫậäåāąAÀÁẢÃẠĂẰẮẲẴẶÂẦẤẨẪẬÄÅĀĄ","cçćčCÇĆČ","dđďDĐĎ","eèéẻẽẹêềếểễệëěēęEÈÉẺẼẸÊỀẾỂỄỆËĚĒĘ","iìíỉĩịîïīIÌÍỈĨỊÎÏĪ","lłLŁ","nñňńNÑŇŃ","oòóỏõọôồốổỗộơởỡớờợöøōOÒÓỎÕỌÔỒỐỔỖỘƠỞỠỚỜỢÖØŌ","rřRŘ","sšśșşSŠŚȘŞ","tťțţTŤȚŢ","uùúủũụưừứửữựûüůūUÙÚỦŨỤƯỪỨỬỮỰÛÜŮŪ","yýỳỷỹỵÿYÝỲỶỸỴŸ","zžżźZŽŻŹ"],r=[];return e.split("").forEach(function(i){n.every(function(n){if(-1!==n.indexOf(i)){if(r.indexOf(n)>-1)return!1;e=e.replace(new RegExp("["+n+"]","gm"+t),"["+n+"]"),r.push(n)}return!0})}),e}},{key:"createMergedBlanksRegExp",value:function(e){return e.replace(/[\s]+/gim,"[\\s]+")}},{key:"createAccuracyRegExp",value:function(e){var t=this,n=this.opt.accuracy,r="string"==typeof n?n:n.value,i="";switch(("string"==typeof n?[]:n.limiters).forEach(function(e){i+="|"+t.escapeStr(e)}),r){case"partially":default:return"()("+e+")";case"complementary":return"()([^"+(i="\\s"+(i||this.escapeStr("!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~¡¿")))+"]*"+e+"[^"+i+"]*)";case"exactly":return"(^|\\s"+i+")("+e+")(?=$|\\s"+i+")"}}}]),e}(),a=function(){function a(e){t(this,a),this.ctx=e,this.ie=!1;var n=window.navigator.userAgent;(n.indexOf("MSIE")>-1||n.indexOf("Trident")>-1)&&(this.ie=!0)}return n(a,[{key:"log",value:function(t){var n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"debug",r=this.opt.log;this.opt.debug&&"object"===(void 0===r?"undefined":e(r))&&"function"==typeof r[n]&&r[n]("mark.js: "+t)}},{key:"getSeparatedKeywords",value:function(e){var t=this,n=[];return e.forEach(function(e){t.opt.separateWordSearch?e.split(" ").forEach(function(e){e.trim()&&-1===n.indexOf(e)&&n.push(e)}):e.trim()&&-1===n.indexOf(e)&&n.push(e)}),{keywords:n.sort(function(e,t){return t.length-e.length}),length:n.length}}},{key:"isNumeric",value:function(e){return Number(parseFloat(e))==e}},{key:"checkRanges",value:function(e){var t=this;if(!Array.isArray(e)||"[object Object]"!==Object.prototype.toString.call(e[0]))return this.log("markRanges() will only accept an array of objects"),this.opt.noMatch(e),[];var n=[],r=0;return e.sort(function(e,t){return e.start-t.start}).forEach(function(e){var i=t.callNoMatchOnInvalidRanges(e,r),o=i.start,a=i.end;i.valid&&(e.start=o,e.length=a-o,n.push(e),r=a)}),n}},{key:"callNoMatchOnInvalidRanges",value:function(e,t){var n=void 0,r=void 0,i=!1;return e&&void 0!==e.start?(r=(n=parseInt(e.start,10))+parseInt(e.length,10),this.isNumeric(e.start)&&this.isNumeric(e.length)&&r-t>0&&r-n>0?i=!0:(this.log("Ignoring invalid or overlapping range: "+JSON.stringify(e)),this.opt.noMatch(e))):(this.log("Ignoring invalid range: "+JSON.stringify(e)),this.opt.noMatch(e)),{start:n,end:r,valid:i}}},{key:"checkWhitespaceRanges",value:function(e,t,n){var r=void 0,i=!0,o=n.length,a=t-o,s=parseInt(e.start,10)-a;return(r=(s=s>o?o:s)+parseInt(e.length,10))>o&&(r=o,this.log("End range automatically set to the max value of "+o)),s<0||r-s<0||s>o||r>o?(i=!1,this.log("Invalid range: "+JSON.stringify(e)),this.opt.noMatch(e)):""===n.substring(s,r).replace(/\s+/g,"")&&(i=!1,this.log("Skipping whitespace only range: "+JSON.stringify(e)),this.opt.noMatch(e)),{start:s,end:r,valid:i}}},{key:"getTextNodes",value:function(e){var t=this,n="",r=[];this.iterator.forEachNode(NodeFilter.SHOW_TEXT,function(e){r.push({start:n.length,end:(n+=e.textContent).length,node:e})},function(e){return t.matchesExclude(e.parentNode)?NodeFilter.FILTER_REJECT:NodeFilter.FILTER_ACCEPT},function(){e({value:n,nodes:r})})}},{key:"matchesExclude",value:function(e){return i.matches(e,this.opt.exclude.concat(["script","style","title","head","html"]))}},{key:"wrapRangeInTextNode",value:function(e,t,n){var r=this.opt.element?this.opt.element:"mark",i=e.splitText(t),o=i.splitText(n-t),a=document.createElement(r);return a.setAttribute("data-markjs","true"),this.opt.className&&a.setAttribute("class",this.opt.className),a.textContent=i.textContent,i.parentNode.replaceChild(a,i),o}},{key:"wrapRangeInMappedTextNode",value:function(e,t,n,r,i){var o=this;e.nodes.every(function(a,s){var c=e.nodes[s+1];if(void 0===c||c.start>t){if(!r(a.node))return!1;var u=t-a.start,l=(n>a.end?a.end:n)-a.start,h=e.value.substr(0,a.start),f=e.value.substr(l+a.start);if(a.node=o.wrapRangeInTextNode(a.node,u,l),e.value=h+f,e.nodes.forEach(function(t,n){n>=s&&(e.nodes[n].start>0&&n!==s&&(e.nodes[n].start-=l),e.nodes[n].end-=l)}),n-=l,i(a.node.previousSibling,a.start),!(n>a.end))return!1;t=a.end}return!0})}},{key:"wrapGroups",value:function(e,t,n,r){return r((e=this.wrapRangeInTextNode(e,t,t+n)).previousSibling),e}},{key:"separateGroups",value:function(e,t,n,r,i){for(var o=t.length,a=1;a-1&&r(t[a],e)&&(e=this.wrapGroups(e,s,t[a].length,i))}return e}},{key:"wrapMatches",value:function(e,t,n,r,i){var o=this,a=0===t?0:t+1;this.getTextNodes(function(t){t.nodes.forEach(function(t){t=t.node;for(var i=void 0;null!==(i=e.exec(t.textContent))&&""!==i[a];){if(o.opt.separateGroups)t=o.separateGroups(t,i,a,n,r);else{if(!n(i[a],t))continue;var s=i.index;if(0!==a)for(var c=1;c + + + + + 用初等数论探索哥德巴赫猜想 - huangjj27's Tech blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+
+

用初等数论探索哥德巴赫猜想

+
+

在探索哥德巴赫猜想在初等数论框架内证明方式, 并由此发现一些显而易见的有趣结论: 若一个偶数2n能够拆分为两个奇素数的和的形式, 并且如果两个奇素数不相等, 那么这两个素数中较小的一个p(易知\( p \lt n \))必然不能整除n.

+
+

哥德巴赫猜想

+

关于历史研究历程一类资料, 请参考wiki.

+

本文将哥德巴赫猜想简单地描述为:

+
+

给定任意整数\(n(n > 1)\), 以及不超过n的所有素数1的集合\( P = \{ p \mid Prime(p) \land p \lt n \} \). 设\(p\)为集合\( P \)中的一个元素, 猜想\(p\)对应的整数\(2n-p\)所组成的集合\(2n-P\)中, 必然存在素数元素.

+
+

引理

+

(一) P中存在不整除n的元素

+

引理1: +\[ \label{1.1} \tag{1.1} +\exists p \in P, p \nmid n +\]

+

证明(反证法):

+

假设命题\( \ref{1.1} \) 的反命题: +\[\label{1.2} \tag{1.2} +\forall p \in P, p \mid n \Leftrightarrow \forall p \in P, \exists m \in Z, m \neq 0 \land mp = n +\] +成立, 由伯特兰-切比雪夫定理(Bertrand's postulate)可知: +\[ +\exists p_0, {n \over 2} \lt p_0 \lt n +\]

+

由假设\( \ref{1.2} \) 可得: +\[{mp_0 \over 2} \lt p_0 \Rightarrow m \lt 2\].

+

又\(m \in Z\), \(p_0 \gt 0, n \gt 0 \Rightarrow m = {n \over p_0} \gt 0\), 所以\(m = 1\), 即\(mp_0 = p_0 = n\), 这与\(p_0 \lt n\) 矛盾!

+

所以命题\( \ref{1.2} \)不成立. 故命题\( \ref{1.1} \)成立.

+

(二) 给定正整数 \(x\) 若整除互质数对\( a, b \)之一,则 \(x\) 不整除 \(a-b\)

+

\[ \label{2} \tag{2} +(a, b) = 1, x \gt 1, a \gt 1, b \gt 1, x|a \lor x|b \Rightarrow x \nmid a-b +\]

+

证明:

+

易知\(x|a\)与\(x|b\)不同时成立, 否则\((a, b) \ge x\), 与 \((a, b) = 1\) 矛盾. +分类讨论:

+
    +
  • +

    \(x|a, , x \nmid b\)

    +

    \( \because x|a \)

    +

    \( \therefore \exists m \ge 1, m \in Z, mx=a. \)

    +

    假设 \( x|a-b \), 即 \( \exists k \in Z, kx=a-b \Leftrightarrow b = a-kx = (m-k)x. \)

    +

    若 \( m = k \), 则 \( b = 0 \), 与 \( b > 1 \) 矛盾;

    +

    若 \( m \neq k \), 则 \( \exists (m-k) \in Z, b = (m-k)x \Leftrightarrow x|b \), 与假设 \( x \nmid b \) 矛盾!

    +

    故假设 \( x|a-b \) 不成立, \( x \nmid a-b. \)

    +
  • +
  • +

    \(x \nmid a, , x|b\). 同理可得: +若\( x|a-b\) , 则 \( a = b + kx = (m+k)x \Leftrightarrow x|a \), 与 \(x \nmid a\) 矛盾! 故 \(x \nmid a-b \).

    +
  • +
+

探索哥德巴赫猜想

+

分类讨论:

+
    +
  • +

    若n为素数, 显然 \( 2n-n = n \) 亦为素数, 哥德巴赫猜想成立. 1

    +
  • +
  • +

    若n为合数, 则 \( n \ge 4 \). 由引理(一), 将集合P以能否整除n划分为以下子集: \( S = \lbrace s \mid s|n \rbrace, T = \lbrace t \mid t \nmid n \rbrace\)

    +

    容易发现以下结论: \( \forall s \in S, \because s|n, s|s, \therefore s|2n-s \), 即若一个素数是n的素因子, 那么对应的整数 \( 2n-s \) 为合数. 故符合哥德巴赫猜想的数对 \( p \)与 \( 2n-p \)必然满足 \( p \nmid n \)(封面结论)

    +

    到这里, 我们可以得到一个哥德巴赫猜想的等价命题:

    +
    +

    对给定合数\(n\), 及小于\(n\)且不整除\(n\)的素数集合\( T = \lbrace t \mid Prime(t) \land t \lt n \land t \nmid n \rbrace \), 在集合\(T\)对应的整数集\(2n-T = \lbrace 2n - t \mid Prime(t) \land t \lt n \land t \nmid n \rbrace \)中是否存在素数

    +
    +
  • +
+

进一步探究

+

推论一

+

\[ \label{3} \tag{3} +\forall s \in S, t \in T, s \nmid 2n-t +\] +而由引理(二)可得:

+

对于任意前述s, t, 有:

+
    +
  1. \( Prime(s), Prime(t) \Rightarrow s \nmid t \)
  2. +
  3. \( s|n \Rightarrow s|2n \)
  4. +
+

故: \( \forall s \in S, t \in T, s \nmid 2n-t \) +也就是, 至少集合 \( 2n-T \) 的元素不会被 \( S \) 中的元素整除, 命题 \( \ref{3} \) 证毕。

+

推论二

+

对于\(T\)的子集 \( T_{\gt {n \over 2}} = T_1 = \lbrace t| t \in T, t \gt {n \over 2} \rbrace \), 具有以下性质:

+

\[ \label{4} \tag{4} +\forall t_1, t_2 \in T_1, t_1 \nmid 2n-t_2 +\]

+

证明如下:

+

假设 \( \exists t_1, t_2, t_1 \mid 2n-t_2 \), 即 \( \exists m \in Z^+, mt_1 = 2n - t_2 \).

+

\( \because t_1, t_2为奇数, 2n为偶数 \) +\( \therefore m为奇数. \)

+

分类讨论:

+
    +
  1. +

    当 \( m = 1 \) 时, t_1 + t_2 = 2n.

    +

    \( \because t_1 \neq t_2, t_1 \lt n, t_2 \lt n, \therefore t_1 + t_2 \lt 2n \), 矛盾!

    +
  2. +
  3. +

    当 \( m \ge 3 \)时:

    +

    \( +\because t_1 \gt {n \over 2}, t_2 \gt {n \over 2}, +\therefore mt_1 + t_2 \gt { mn \over 2 } + { n \over 2 } \ge { 3n \over 2 } + { n \over 2 } = 2n +\) +, 矛盾!

    +
  4. +
+

故假设不成立, 命题 \( \ref{4} \) 证毕.

+
1 +

如无特殊说明,本文中所有集合均为正整数集的子集

+
+
2 +

在本文中暂且抛去哥德巴赫猜想中对分解出的两个素数不能相等的要求。

+
+ +
+ +
+ + + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + diff --git a/number_theory/if_2n-p_is_divided_by_p.html b/number_theory/if_2n-p_is_divided_by_p.html new file mode 100644 index 0000000..49ca308 --- /dev/null +++ b/number_theory/if_2n-p_is_divided_by_p.html @@ -0,0 +1,262 @@ + + + + + + 若质数p不能整除偶数2n,则p不整除2n-p - huangjj27's Tech blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+
+

若质数p不能整除偶数2n,则p不整除2n-p

+

命题形式化描述

+

\[\label{1} \tag{1} +\forall p \in Primes, p \lt 2n(n \in Z^+), p \nmid 2n \Rightarrow p \nmid 2n - p +\]

+

一般化命题与反证法 1

+

我们可以抽出一个更一般的命题:对任意正整数 \(a, b (a \lt b)\),若 \( a \nmid b\),则 \( a \nmid b - a \): +\[\label{1.0} \tag{1.0} +\forall a, b \in Z^+ (a \lt b), a \nmid b \Rightarrow a \mid b - a +\]

+

反证法

+

假设原结论不成立,即 \( a \mid b - a \) 成立,则:

+

\( \because a \mid b - a, a \mid a \)

+

\( \therefore a \mid (b - a) + a \) (整除的线性性质),即 \( a \mid b \),

+

这与假设 \( a \nmid b \) 矛盾!故假设不成立,原结论成立,即 \( a\mid b - a \),

+

故命题 \( \ref{1.0} \) 成立。

+

代入条件

+

令 \( a := p, b = 2n \), 则显然:

+

\[ +p \in Primes, p \lt 2n \Rightarrow p \nmid 2n \Rightarrow p \nmid 2n - p +\]

+

即命题 \( \ref{1} \) 得证。

+
+

(以下内容为作者之前思考过的另外一个比较迂回的证明方法,为后续叙述的引理)

+

一整数若不同时整除互质两数,则不能整除后两数之差

+

形式化描述

+

\[ \label{1.1} \tag{1.1} +\forall x \in Z^+ \land x > 1, a \in Z^+ \land a > 1, b \in Z^+ \land b > 1; \quad +(a, b) = 1, x|a \lor x|b \Rightarrow x \nmid a-b +\]

+

证明

+

不妨设 \( a \gt b \)。易知\(x|a\)与\(x|b\)不同时成立, 否则\((a, b) \ge x\), 与 \((a, b) = 1\) 矛盾. +分类讨论:

+
    +
  • +

    \(x|a, , x \nmid b\)

    +

    \( \because x|a \)

    +

    \( \therefore \exists m \in Z^+, mx=a. \)

    +

    假设 \( x|a-b \), 即 \( \exists k \in Z^+, kx=a-b \Leftrightarrow b = a-kx = (m-k)x. \)

    +

    若 \( m = k \), 则 \( b = 0 \), 与 \( b > 1 \) 矛盾;

    +

    若 \( m \neq k \), 则 \( \exists (m-k) \in Z, b = (m-k)x \Leftrightarrow x|b \), 与假设 \( x \nmid b \) 矛盾!

    +

    故假设 \( x|a-b \) 不成立, \( x \nmid a-b. \)

    +
  • +
  • +

    \(x \nmid a, , x|b\). 同理可得: +若\( x|a-b\) , 则 \( a = b + kx = (m+k)x \Leftrightarrow x|a \), 与 \(x \nmid a\) 矛盾! 故 \(x \nmid a-b \).

    +
  • +
+

代入条件

+

令 \( x := p, a := 2n, b := p \),显然有\( p \mid p \)。故,当 \(p \nmid 2n \)时, \( p \nmid 2n - p \),即命题 \( \ref{1} \) 得证。

+
1 +

感谢中山大学数学专业的倪秉业师弟指出这个更简洁的证明方式!

+
+ +
+ +
+ + + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + diff --git a/number_theory/intro.html b/number_theory/intro.html new file mode 100644 index 0000000..b26631f --- /dev/null +++ b/number_theory/intro.html @@ -0,0 +1,225 @@ + + + + + + 初等数论自我探索 - huangjj27's Tech blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+
+

初等数论自我探索

+

本系列文章以解决哥德巴赫猜想为目的(证明/证伪),把思路限制在初等数论(个人水平有限,理解不了更高级的工具),进行思考与尝试,本意是打发自己零散的通勤时间,走到哪算到哪。

+

该系列文章的最初设想是有一天突然就想到,关于一个偶数 \( 2n (n \in Z^+) \)的素数相加的分拆,一定是关于整数 \( n \) 对称的,即若 \( 2n = p_1 + p_2 \),其中 \( p_1 \), \( p_2 \) 为质数(\(p_1 \neq p_2 \)),那么: +\[ +\exists d \in Z^+, +\begin{cases} +p_1 = n - d \\ +p_2 = n + d +\end{cases} +\]

+

那么这个 \( d \) 和 \( n \) 有什么关系呢?适逢当时被科普了 黎曼猜想, 得知黎曼 \( \Zeta \) 函数的非平凡零点很可能全部都位于直线 \( Re(z) = {1 \over 2} \) 上,那么会不会,最小的 \( d \) 也会在 \( n \cdot { 1 \over 2} \) 的边界内找得到呢?

+

于是提出了以下猜想命题: +\[ \tag{0} \label{0} +\forall n \in Z^+, n \gt M, \exists d \in Z^+, d \le { n \over 2 }, Prime(n - d) \land Prime(n + d) +\]

+

其中 \( M \) 为常数,表示命题成立在某个整数以上的整数范围成立。也就是,证明有穷个整数之后的整数都满足性质\( \ref{0} \),然后再逐个验证之前的整数满足该性质,就可以得出一个比偶数哥德巴赫猜想更充分的命题,从而直接可以推导出偶数哥德巴赫定理。

+ +
+ +
+ + + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + diff --git a/number_theory/single-composite-divsion.html b/number_theory/single-composite-divsion.html new file mode 100644 index 0000000..945b90c --- /dev/null +++ b/number_theory/single-composite-divsion.html @@ -0,0 +1,250 @@ + + + + + + 整数和它两倍间的合数的性质 - huangjj27's Tech blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+
+

整数和它两倍(一倍半)之间的合数,可以整除整数的阶乘

+

形式化描述

+

\[ \label{2.1} \tag{2.1} +\forall m \in Z^+, m \gt 8, n \in Z^+, m \lt n \lt 2m; \quad n \not \in Primes \Leftrightarrow n \mid m! +\]

+

证明

+

充分性

+

\[ \label{2.1.1} \tag{2.1.1} +n \not \in Primes \Rightarrow n \mid m! +\]

+

因为 \( n \) 为合数,将其分为完全平方数和不完全平方数两种情况证明。

+

不完全平方数

+

不妨设 \( n = ab (a \gt b \ge 2, (a, b) = 1) \),则 \(b \lt a \lt m \),证明如下:

+

若 \( a \ge m \),

+

则 \(n = ab \ge 2a \ge 2m \Leftrightarrow n \ge 2m \),

+

这与 \( n \lt 2m \) 矛盾!

+

故假设不成立,故 \(2 \le b \lt a \lt m \Rightarrow a \mid m!, b \mid m! \)。

+

又 \( (a, b) = 1 \Rightarrow [a, b] = ab = n \)

+

因此:\( a \mid m!, b \mid m \Rightarrow [a, b] \mid m! \Leftrightarrow n \mid m! \)。

+

完全平方数

+

不妨设 \( n = k^2 \),则 \(2 \lt k \lt 2k \lt m \),证明如下:

+

若 \( 2k \ge m \),则 \( k \ge { m \over 2 } \),

+

则 \(n = k^2 \ge { m^2 \over 4 } \ge { 8 / 4 } \cdot m = 2m \Leftrightarrow n \ge 2m \),

+

这与 \( n \lt 2m \) 矛盾!

+

故假设不成立,故 \( 2 \le k \lt 2k \lt m \Rightarrow 2k^2 \mid m! \)。

+

因此:\( 2k^2 \mid m! \Leftrightarrow 2n | m! \Rightarrow n \mid m! \)。

+

综上,命题 \( \ref{2.1.1} \) 得证。

+

必要性

+

\[ \label{2.1.2} \tag{2.1.2} +n \mid m! \Rightarrow n \not \in Primes +\]

+

反证:假设 \(n \in Primes \),则:

+

\( \because n > m, n \in Primes, \)

+

\( \therefore n \nmid 2, \space n \nmid 3, \space n \nmid 4, \space \dots, \space n \mid m \Rightarrow n \nmid m!, \)

+

这与条件 \( n \mid m! \) 矛盾!故假设不成立,命题 \( \ref{2.1.2} \) 得证。

+

综上,命题 \( \ref{2.1} \) 得证。

+

更强的结论

+

显然对 \( \forall n \in (m, 3/2m) \Rightarrow n \in (m, 2n) \),故: +\[ \label{2.2} \tag{2.2} +\forall m \in Z^+, m \gt 8, n \in Z^+, m \lt n \lt { 3m \over 2 }; \quad n \not \in Primes \Leftrightarrow n \mid m! +\]

+ +
+ +
+ + + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + diff --git a/print.html b/print.html new file mode 100644 index 0000000..298537b --- /dev/null +++ b/print.html @@ -0,0 +1,3702 @@ + + + + + + huangjj27's Tech blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+
+

由 tauri 单例模式 bug “意外修复” 发现的 dangling

+

TL;DR

+

tauri 单例插件 用于区分单例实例的 productName的过长会导致单例功能失效,博主最初确信 encode_wide 实现有问题,并提交了修复。然而在和社区深入研究问题原因后,发现根本原因是使用 encode_wide 转码传参时造成了 dangling

+

PS: 为方便读者理解,博主花费一天时间重新梳理分析步骤,按照演绎法展示定位 bug 地过程,实现发生的分析过程要比博文的过程更加曲折,对分析理解问题无意义因此略过。

+

所以这个 bug 的现象是怎么样的?

+

正如博主所说,在 有问题版本的插件代码 中,博主最初发现,tauri.conf.json 中的 package.productName 在分别使用五个汉字与六个汉字时,单例模式功能表现出不一致的行为:五个汉字的 productName 单例功能运行正常,而六个汉字的 productName 单例功能失效,于是针对该问题初步进行了测试:

+
+ + + +
测试的 productName单例插件功能是否生效
六个汉字试试x
随便五个字
又来了六个字x
+
+

因为这些汉字测试用例使用 UTF-8 编码,又因为是常用字,因此每个汉字对应 3 bytes,因此假设 productName 在超过 15 bytes、不超过 18 bytes 时会导致功能失效,进一步补充测试用例:

+
+ + +
测试的 productName单例插件功能是否生效
z12345678901234
z123456789012345x
+
+

看来博主运气不错,刚好踩到了边界的测试用例。那么基本可以确定,productName 超过 15 bytes 就会导致单例功能失效。

+

PotatoTooLarge: 传递给 Win32 API 的字符串要用 C string 风格的 \0 结束

+

在最初的讨论过程中,因为我们没有仔细留意插件仓库使用的 encode_wide 是自行做过封装的,因此我们一开始根据 以下代码 进行分析:

+
pub fn init<R: Runtime>(f: Box<SingleInstanceCallback<R>>) -> TauriPlugin<R> {
+    plugin::Builder::new("single-instance")
+        .setup(|app| {
+            let app_name = &app.package_info().name;
+            let class_name = format!("{}-single-instance-class", app_name);
+            let window_name = format!("{}-single-instance-window", app_name);
+
+            let hmutex = unsafe {
+                CreateMutexW(
+                    std::ptr::null(),
+                    true.into(),
+                    encode_wide("tauri-plugin-single-instance-mutex").as_ptr(),
+                )
+            };
+
+            if unsafe { GetLastError() } == ERROR_ALREADY_EXISTS {
+                unsafe {
+                    let hwnd = FindWindowW(
+                        encode_wide(&class_name).as_ptr(),
+                        encode_wide(&window_name).as_ptr(),
+                    );
+
+                    // omitted
+                }
+
+                // omitted
+            }
+
+            // omitted
+        })
+        .on_event(|app, event| {
+            // omitted
+        })
+        .build()
+}
+
+

有 Windows 编程经验的 @PotatoTooLarge 指出,encode_wide(来自 std 的 Window扩展)并不会补充 \0 结束符:

+
+

Re-encodes an OsStr as a wide character sequence, i.e., potentially ill-formed UTF-16.

+

This is lossless: calling OsStringExt::from_wide and then encode_wide on the result will yield the original code units. Note that the encoding does not add a final null terminator.

+
+

于是博主听取建议,把所有会传递到 encode_wide 函数的字符串都添加了 \0,形成了 一版修复:

+
pub fn init<R: Runtime>(f: Box<SingleInstanceCallback<R>>) -> TauriPlugin<R> {
+    plugin::Builder::new("single-instance")
+        .setup(|app| {
+            let app_name = &app.package_info().name;
+            // let class_name = format!("{}-single-instance-class", app_name);
+            let class_name = format!("{}-single-instance-class\0", app_name);
+            // let window_name = format!("{}-single-instance-window", app_name);
+            let window_name = format!("{}-single-instance-window\0", app_name);
+
+            let hmutex = unsafe {
+                CreateMutexW(
+                    std::ptr::null(),
+                    true.into(),
+                    // encode_wide("tauri-plugin-single-instance-mutex").as_ptr(),
+                    encode_wide("tauri-plugin-single-instance-mutex\0").as_ptr(),
+                )
+            };
+
+            if unsafe { GetLastError() } == ERROR_ALREADY_EXISTS {
+                unsafe {
+                    let hwnd = FindWindowW(
+                        encode_wide(&class_name).as_ptr(),
+                        encode_wide(&window_name).as_ptr(),
+                    );
+
+                    // omitted
+                }
+
+                // omitted
+            }
+
+            // omitted
+        })
+        .on_event(|app, event| {
+            // omitted
+        })
+        .build()
+}
+
+

然后使用修复前会引起单例功能失效的 z123456789012345 作为测试用例,验证单例功能可用了,证明该修改可以修复单例功能失效的问题。

+

但它并不是真的修复

+

在博主提交了修复后,插件仓库作者提醒,前文所述的代码使用的 encode_wide封装拼接了 \0 后再传递参数的:

+
pub fn encode_wide(string: impl AsRef<std::ffi::OsStr>) -> Vec<u16> {
+    std::os::windows::prelude::OsStrExt::encode_wide(string.as_ref())
+        .chain(std::iter::once(0))
+        .collect()
+}
+
+

这意味着,并不是 \0 导致问题的失效,因为该函数在 windows 环境下执行是能够补足 \0 的:

+
fn encode_wide(string: impl AsRef<std::ffi::OsStr>) -> Vec<u16> {
+    std::os::windows::prelude::OsStrExt::encode_wide(string.as_ref())
+        .chain(std::iter::once(0))
+        .collect()
+}
+
+fn main() {
+    let product_name = "z123456789012345";
+
+    // output: [122, 49, 50, 51, 52, 53, 54, 55, 56, 57, 48, 49, 50, 51, 52, 53, 0]
+    //                                                                           ^
+    //                              null concated here so it's null-terminated --|
+    println!("{:?}", encode_wide(product_name));
+}
+
+

那么失效的过程发生了什么?

+

为了分析问题详细过程,我将插件仓库代码切换到了 问题代码版本

+
git checkout 16e5e9eb59da9ceca3dcf09c81120b37fe108a03
+
+

然后添加了一些 dbg 宏:

+
pub fn init<R: Runtime>(f: Box<SingleInstanceCallback<R>>) -> TauriPlugin<R> {
+    plugin::Builder::new("single-instance")
+        .setup(|app| {
+            let app_name = &app.package_info().name;
+            let class_name = format!("{}-single-instance-class", app_name);
+            let window_name = format!("{}-single-instance-window", app_name);
+
+            let hmutex = unsafe {
+                CreateMutexW(
+                    std::ptr::null(),
+                    true.into(),
+                    encode_wide("tauri-plugin-single-instance-mutex").as_ptr(),
+                )
+            };
+            dbg!(hmutex);  // windows.rs:43 debug here!
+
+            if unsafe { GetLastError() } == ERROR_ALREADY_EXISTS {
+                unsafe {
+                    let hwnd = FindWindowW(
+                        encode_wide(&class_name).as_ptr(),
+                        encode_wide(&window_name).as_ptr(),
+                    );
+                    dbg!(hwnd);  // windows.rs:51 debug here!
+
+                    // omitted
+                }
+            } else {
+                app.manage(MutexHandle(hmutex));
+
+                let hwnd = create_event_target_window::<R>(&class_name, &window_name);
+                dbg!(hwnd);  // windows.rs:76 debug here!
+
+                // omitted
+            }
+
+            Ok(())
+        })
+        .on_event(|app, event| {
+            // omitted
+        })
+        .build()
+}
+
+

然后,将代码仓库 examples\emit-event\src-tauri\tauri.conf.json 分别改成 z12345678901234z123456789012345,然后执行:

+
# process1
+> cd examples\emit-event
+examples\emit-event> cargo tauri build --debug
+examples\emit-event> src-tauri\target\debug\z12345678901234.exe
+[tauri-plugin-single-instance\src\platform_impl\windows.rs:43] hmutex = 548
+[tauri-plugin-single-instance\src\platform_impl\windows.rs:76] hwnd = 40113446
+
+# process2
+> examples\emit-event\src-tauri\target\debug\z12345678901234.exe
+[tauri-plugin-single-instance\src\platform_impl\windows.rs:43] hmutex = 548
+[tauri-plugin-single-instance\src\platform_impl\windows.rs:51] hwnd = 40113446
+
+
# process1
+> cd examples\emit-event
+examples\emit-event> cargo tauri build --debug
+examples\emit-event> src-tauri\target\debug\z123456789012345.exe
+[tauri-plugin-single-instance\src\platform_impl\windows.rs:43] hmutex = 552
+[tauri-plugin-single-instance\src\platform_impl\windows.rs:76] hwnd = 0
+
+# process2
+> examples\emit-event\src-tauri\target\debug\z123456789012345.exe
+[tauri-plugin-single-instance\src\platform_impl\windows.rs:43] hmutex = 548
+[tauri-plugin-single-instance\src\platform_impl\windows.rs:51] hwnd = 0
+
+# process3
+> examples\emit-event\src-tauri\target\debug\z123456789012345.exe
+[tauri-plugin-single-instance\src\platform_impl\windows.rs:43] hmutex = 548
+[tauri-plugin-single-instance\src\platform_impl\windows.rs:51] hwnd = 0
+
+

由上面的结果我们可以知道:在用例 z12345678901234,我们在创建实实例时返回了有效的 hwnd 值,并且在检查 hwnd 时确认已经创建窗口;而在用例 z123456789012345,我们创建窗口的函数 create_event_target_window 返回的 hwnd 是无效的!所以导致问题的代码,应该在 create_event_target_window 的逻辑中!再次添加 dbg ,重新编译后继续 debug:

+
fn create_event_target_window<R: Runtime>(class_name: &str, window_name: &str) -> HWND {
+    unsafe {
+        let class = WNDCLASSEXW {
+            cbSize: std::mem::size_of::<WNDCLASSEXW>() as u32,
+            style: 0,
+            lpfnWndProc: Some(single_instance_window_proc::<R>),
+            cbClsExtra: 0,
+            cbWndExtra: 0,
+            hInstance: GetModuleHandleW(std::ptr::null()),
+            hIcon: 0,
+            hCursor: 0,
+            hbrBackground: 0,
+            lpszMenuName: std::ptr::null(),
+            lpszClassName: encode_wide(&class_name).as_ptr(),
+            hIconSm: 0,
+        };
+        dbg!(class.lpszClassName);  // windows.rs:153 debug here
+        dbg!(*class.lpszClassName);  // windows.rs:154 debug here
+
+        RegisterClassExW(&class);
+
+        let hwnd = CreateWindowExW(
+            WS_EX_NOACTIVATE
+            | WS_EX_TRANSPARENT
+            | WS_EX_LAYERED
+            // WS_EX_TOOLWINDOW prevents this window from ever showing up in the taskbar, which
+            // we want to avoid. If you remove this style, this window won't show up in the
+            // taskbar *initially*, but it can show up at some later point. This can sometimes
+            // happen on its own after several hours have passed, although this has proven
+            // difficult to reproduce. Alternatively, it can be manually triggered by killing
+            // `explorer.exe` and then starting the process back up.
+            // It is unclear why the bug is triggered by waiting for several hours.
+            | WS_EX_TOOLWINDOW,
+            dbg!(encode_wide(&class_name).as_ptr()),  // windows.rs:170 debug here
+            dbg!(encode_wide(&window_name).as_ptr()),  // windows.rs:171 debug here
+            WS_OVERLAPPED,
+            0,
+            0,
+            0,
+            0,
+            0,
+            0,
+            GetModuleHandleW(std::ptr::null()),
+            std::ptr::null(),
+        );
+        SetWindowLongPtrW(
+            hwnd,
+            GWL_STYLE,
+            // The window technically has to be visible to receive WM_PAINT messages (which are used
+            // for delivering events during resizes), but it isn't displayed to the user because of
+            // the LAYERED style.
+            (WS_VISIBLE | WS_POPUP) as isize,
+        );
+        hwnd
+    }
+}
+
+

z12345678901234:

+
examples\emit-event> src-tauri\target\debug\z12345678901234.exe
+[tauri-plugin-single-instance\src\platform_impl\windows.rs:43] hmutex = 556
+[tauri-plugin-single-instance\src\platform_impl\windows.rs:153] class.lpszClassName = 0x0000021d099eddc0
+[tauri-plugin-single-instance\src\platform_impl\windows.rs:154] *class.lpszClassName = 122
+[tauri-plugin-single-instance\src\platform_impl\windows.rs:170] encode_wide(&class_name).as_ptr() = 0x0000021d099ee180
+[tauri-plugin-single-instance\src\platform_impl\windows.rs:171] encode_wide(&window_name).as_ptr() = 0x0000021d099dfe20
+[tauri-plugin-single-instance\src\platform_impl\windows.rs:76] hwnd = 28841288
+
+

z123456789012345:

+
examples\emit-event> src-tauri\target\debug\z123456789012345.exe
+[tauri-plugin-single-instance\src\platform_impl\windows.rs:43] hmutex = 548
+[tauri-plugin-single-instance\src\platform_impl\windows.rs:153] class.lpszClassName = 0x0000017259ca6be0
+[tauri-plugin-single-instance\src\platform_impl\windows.rs:154] *class.lpszClassName = 43920
+[tauri-plugin-single-instance\src\platform_impl\windows.rs:170] encode_wide(&class_name).as_ptr() = 0x0000017259cc6b30
+[tauri-plugin-single-instance\src\platform_impl\windows.rs:171] encode_wide(&window_name).as_ptr() = 0x0000017259cc6970
+[tauri-plugin-single-instance\src\platform_impl\windows.rs:76] hwnd = 0
+
+

对比我们之前 encode_wide 函数返回的结果class_name 开头的字符应该是 ASCII 字符 z(ASCII 码 122),因此通过 encode_wide(&class_name).as_ptr() 传参的 class.lpszClassName,应当指向值为 "z123456789012345-single-instance-class" 的字符串,这在用例 z12345678901234 中行为符合预期;但在 z123456789012345 的用例中,class.lpszClassName 指向的却发生了变化(*class.lpszClassName = 43920),反推可以得知, encode_wide(&class_name).as_ptr() 并没有成功地把指针传递给 class.lpszClassName

+

一语惊醒梦中人:悬垂指针(dangling)!

+

@Berrysoft 指出, encode_wide(&class_name).as_ptr() 这种写法由于直接对临时变量直接取指针,而临时变量 encode_wide(&class_name) 会在执行完之后被马上释放结束生命周期,因此指向该临时变量的指针也会变成悬垂指针!临时变量的这一行为在 reference 中有说明:

+
+

When using a value expression in most place expression contexts, a temporary unnamed memory location is created and initialized to that value. The expression evaluates to that location instead, except if promoted to a static. The drop scope of the temporary is usually the end of the enclosing statement.

+
+

而解决该问题,只需要把提升变量的 lifetime,把要用到的变量提取出来,使其 lifetime 可以覆盖要用到的函数而不至于在语句执行完之后马上被回收。于是有了解决问题的 PR

+

那为什么在 format 的时候手动添加 \0 后,问题“修复”了呢?

+

这个问题依然悬而未决。有 TG 群友提出,可能是由于堆栈被破坏 “碰巧” 又指向了正确的字符串位置,而 format 后的变量又是 'static 的,因此能达到“修复”的效果,然而这依然是基于 bug/undefined behavior 的修复方案,因此仍然不可靠。后续原因排查出来后会更新博客~

+

教训与经验

+
    +
  1. 实际上的排查过程,是分析过一次 create_event_target_window 的,然而当时由于需求紧急而找到了临时绕开的实现方案(把 productName 砍短),因此搁置了,也没有留下相关的排查记录文档,以致于后续在需求变更而变得必须排查清楚该问题时,走向了排查 encode_wide 的错误方向,虽然有了“修复”方案,但该方案仍然不可靠,因此可以视作浪费了实践。 形成记录首先方便的是以后的自己。
  2. +
  3. 凡是 unsafe 多查几遍。像本文涉及到的悬垂指针问题,在 safe rust 中因为 lifetime 不够长而会阻止编译,而 unsafe 块中使用裸指针是不会被编译器检查的,因此相关操作都要相当慎重。
  4. +
  5. 多借助社区的力量。比起一个人钻牛角尖,多与社区讨论才容易跳出原本的死胡同,从而理解意识到原来思路的局限性。
  6. +
+

尝试在单 HTML 文件中嵌入 WASM 模块的错误操作

+

TL;DR

+
    +
  1. 当项目是需要 WASM 与 JavaScript 相互交互的时候,请尽可能在统一的 JavaScript 入口中定义所有的功能;
  2. +
  3. wasm-pack 生成的胶水 JavaScript 与 WASM 可以稍作修改即可嵌入到 HTML 文件中。
  4. +
+

项目背景

+

当博主兴高采烈地使用 HTML 与 JavaScript 迅速开发好 UI 界面与交互功能的时候,发现核心的功能的 JavaScript 库只支持 npm 环境而无法应用到前述 UI 界面上,博主迫于无奈只能抓起以前做过的 Rust 版本库,尝试改造成 WASM 模块以复用界面代码。因为博主的这个项目是属于不对外开放的项目,因此本文中使用的项目是简化后的 demo,但不影响博主记录以及提醒上述遇到的两个问题(这两个坑每一个都坑掉了我几个小时,但愿会有读者看到我这篇文章抢救一下自己的时间)。

+

demo 项目架构

+
demo
+  |-- Cargo.toml
+  |-- src
+        |-- lib.rs
+  |-- assets
+        |-- demo.html
+
+
# Cargo.toml
+[package]
+name = "demo"
+authors = ["huangjj27 <huangjj.27@qq.com>"]
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[lib]
+crate-type = ["cdylib", "rlib"]
+
+[dependencies]
+wasm-bindgen = "0.2.63"
+
+
// lib.rs
+use wasm_bindgen::prelude::*;
+
+#[wasm_bindgen]
+pub fn add(a: i32, b: i32) -> i32 {
+    a + b
+}
+
+
<!-- demo.html -->
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>just a demo</title>
+    <script id="indexscript" type="module">
+        import init, { add } from "../pkg/demo.js";
+        async function run() {
+            await init();
+        }
+
+        run();
+    </script>
+    <script>
+        function js_add() {
+            let a = document.getElementById("a");
+            let b = document.getElementById("b");
+            let sum = document.getElementById("sum");
+            sum.value = add(a.value, b.value);
+        }
+    </script>
+</head>
+<body>
+    <textarea id="a" autofocus oninput="js_add()">40</textarea>
+    <textarea id="b" oninput="js_add()">2</textarea>
+    <textarea id="sum" readonly></textarea>
+</body>
+</html>
+
+

项目在编译的时候还需要 wasm-pack,用来生成胶水 JavaScript demo/pkg/demo.js 和 wasm 文件 demo/pkg/demo_bg.wasm

+
wasm-pack build --target=web
+
+

薛定谔的 JavaScript 函数

+

当我们打开 demo.html 并且尝试修改 ab 的值时,我们会从控制台遇到了如下报错:

+
13:33:19.672 Uncaught ReferenceError: add is not defined
+    js_add file:///demo/assets/demo.html:23
+    oninput file:///demo/assets/demo.html:1
+2 demo.html:22:13
+
+

这里的 add 函数就是我们从 WASM 模块中加载的,来自 rust 实现的 add 函数。此时,如果我们在 run 下属下方,直接调用 add 则是可以执行成功的:

+
    <script id="indexscript" type="module">
+        import init, { add } from "../pkg/demo.js";
+        async function run() {
+            await init();
+            console.log(`add函数已加载,add(1, 2) = ${add(1, 2)}`);
+        }
+
+        run();
+    </script>
+
+

执行结果:

+
14:02:58.360 add函数已加载,add(1, 2) = 3 demo.html:13:21
+14:03:11.895 Uncaught ReferenceError: add is not defined
+    js_add file:///demo/assets/demo.html:23
+    oninput file:///demo/assets/demo.html:1
+2 demo.html:23:13
+
+

出现以上现象的原因是,type="module" 限制了 indexscript 内部项目的作用域只能在该 <script> 代码块中有效。解决方法有两种:

+
    +
  1. 将需要导出的给其他 <script> 块使用的功能,挂载在页面全局的 window 对象上,模拟 export 的效果,缺点是很可能无意中覆盖了挂载对象。
  2. +
  3. 将所有js 功能都集中在 indexscript 代码块中,将所有的功能统一管理。此时,要注意将DOM 元素的事件通过监听事件的方式来管理:
  4. +
+
<!-- demo.html -->
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>just a demo</title>
+    <script id="indexscript" type="module">
+        import init, { add } from "../pkg/demo.js";
+        function js_add() {
+            let a = document.getElementById("a");
+            let b = document.getElementById("b");
+            let sum = document.getElementById("sum");
+            sum.value = add(a.value, b.value);
+        }
+
+        async function run() {
+            await init();
+
+            document.getElementById("a").addEventListener("input", js_add);
+            document.getElementById("b").addEventListener("input", js_add);
+        }
+
+        run();
+    </script>
+</head>
+<body>
+    <textarea id="a" autofocus>40</textarea>
+    <textarea id="b">2</textarea>
+    <textarea id="sum" readonly></textarea>
+</body>
+</html>
+
+

然而我还是想包容你的,我的 WASM

+

我们来分析一下 wasm-pack 生成的 demo.js

+
// demo.js
+
+let wasm;
+
+/**
+* @param {number} a
+* @param {number} b
+* @returns {number}
+*/
+export function add(a, b) {
+    const ret = wasm.add(a, b);
+    return ret;
+}
+
+async function load(module, imports) {
+    if (typeof Response === 'function' && module instanceof Response) {
+        if (typeof WebAssembly.instantiateStreaming === 'function') {
+            try {
+                return await WebAssembly.instantiateStreaming(module, imports);
+
+            } catch (e) {
+                if (module.headers.get('Content-Type') != 'application/wasm') {
+                    console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e);
+
+                } else {
+                    throw e;
+                }
+            }
+        }
+
+        const bytes = await module.arrayBuffer();
+        return await WebAssembly.instantiate(bytes, imports);
+
+    } else {
+        const instance = await WebAssembly.instantiate(module, imports);
+
+        if (instance instanceof WebAssembly.Instance) {
+            return { instance, module };
+
+        } else {
+            return instance;
+        }
+    }
+}
+
+async function init(input) {
+    if (typeof input === 'undefined') {
+        input = new URL('demo_bg.wasm', import.meta.url);
+    }
+    const imports = {};
+
+
+    if (typeof input === 'string' || (typeof Request === 'function' && input instanceof Request) || (typeof URL === 'function' && input instanceof URL)) {
+        input = fetch(input);
+    }
+
+
+
+    const { instance, module } = await load(await input, imports);
+
+    wasm = instance.exports;
+    init.__wbindgen_wasm_module = module;
+
+    return wasm;
+}
+
+export default init;
+
+

我们看到,demo.js:175 对来待加载的 module 做了判断:如果不是从响应获取的数据,则直接视作 wasm bytes 来进行加载。于是我们可以对生成的 demo_bg.wasm 文件通过 base64 转码,嵌入到 HTML 文件中,然后转换成 Uint8Array 传递给 demo.js:init 函数进行加载:

+
    <script id="indexscript" type="module">
+// demo.js 完全嵌入到 html 文件中,并且不需要 export 语句
+let wasm;
+
+/**
+* @param {number} a
+* @param {number} b
+* @returns {number}
+*/
+function add(a, b) {
+    const ret = wasm.add(a, b);
+    return ret;
+}
+
+async function load(module, imports) {
+    if (typeof Response === 'function' && module instanceof Response) {
+        if (typeof WebAssembly.instantiateStreaming === 'function') {
+            try {
+                return await WebAssembly.instantiateStreaming(module, imports);
+
+            } catch (e) {
+                if (module.headers.get('Content-Type') != 'application/wasm') {
+                    console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e);
+
+                } else {
+                    throw e;
+                }
+            }
+        }
+
+        const bytes = await module.arrayBuffer();
+        return await WebAssembly.instantiate(bytes, imports);
+
+    } else {
+        const instance = await WebAssembly.instantiate(module, imports);
+
+        if (instance instanceof WebAssembly.Instance) {
+            return { instance, module };
+
+        } else {
+            return instance;
+        }
+    }
+}
+
+async function init(input) {
+    if (typeof input === 'undefined') {
+        input = new URL('demo_bg.wasm', import.meta.url);
+    }
+    const imports = {};
+
+
+    if (typeof input === 'string' || (typeof Request === 'function' && input instanceof Request) || (typeof URL === 'function' && input instanceof URL)) {
+        input = fetch(input);
+    }
+
+
+
+    const { instance, module } = await load(await input, imports);
+
+    wasm = instance.exports;
+    init.__wbindgen_wasm_module = module;
+
+    return wasm;
+}
+
+// demo.js 嵌入结束
+
+        // wasm 二进制,base64编码:
+        const WASM_B64 = "AGFzbQEAAAABBwFgAn9/AX8DAgEABQMBABEHEAIGbWVtb3J5AgADYWRkAAAKCQEHACAAIAFqCwB7CXByb2R1Y2VycwIIbGFuZ3VhZ2UBBFJ1c3QADHByb2Nlc3NlZC1ieQMFcnVzdGMdMS42MS4wIChmZTViMTNkNjggMjAyMi0wNS0xOCkGd2FscnVzBjAuMTkuMAx3YXNtLWJpbmRnZW4SMC4yLjgwICg0Y2FhOTgxNjUp";
+
+        function b64toBytes(b64) {
+            let binary = atob(b64);
+            let bytes = new Uint8Array(binary.length);
+            for (let i = 0; i < bytes.length; i++) {
+                bytes[i] = binary.charCodeAt(i);
+            }
+            return bytes;
+        }
+
+        function js_add() {
+            let a = document.getElementById("a");
+            let b = document.getElementById("b");
+            let sum = document.getElementById("sum");
+            sum.value = add(a.value, b.value);
+        }
+
+        async function run() {
+            await init(b64toBytes(WASM_B64));  // 直接通过页面加载编码
+
+            document.getElementById("a").addEventListener("input", js_add);
+            document.getElementById("b").addEventListener("input", js_add);
+        }
+
+        run();
+    </script>
+
+

完成以上步骤,我们就得到了一个带有 WASM 功能的 standalone html 文件啦~。

+

蓄水池算法改进 - 面向抽奖场景保证等概率性

+
+

免责声明:禁止任何个人或团体使用本文研究成果用于实施任何违反中华人民共和国法律法规的活动 +如有违反,均与本文作者无关

+
+

在我们通常遇到的抽奖场景,于年会时将所有人的编号都放到箱子里面抽奖,然后每次抽出中奖者 +决定奖项。而在这过程中,因为先抽中者已经确定了奖项,然后不能够参与后续的奖项的抽奖;而后 +续参与抽奖的人员则其实会以越来越低的概率参与抽奖:

+
+

例:在上述场景中共有 \( n \) 人参与抽取 \( m ( \lt n) \) 个奖项,

+

抽取第一个奖项概率为: \( { m \over n } \)

+

那么如果第一个奖项被抽走并 揭露了,剩下 \( n - 1 \) 人参与 \( m - 1 \) 个奖项,抽中的概率 +为 \( m - 1 \over n - 1 \)。 +那么 \( m \lt n \Rightarrow -m \gt -n \Rightarrow mn - m \gt nm - n \Rightarrow m(n-1) \gt n(m - 1) \Rightarrow { m \over n } \gt { m - 1 \over n - 1 }\), +即如果是后续参与抽奖 并且前面的奖项被拿走了,后面抽到奖项的概率会更低,同时也会失去参与部分奖项的机会

+
+

因此,在人数 \( n \) 大于奖项数 \( m \) 的时候,我们通过以越来越低的概率干涉前面 +已经“取得”奖项的结果,来保证先参与抽奖的人中奖的概率随着人数的增多中奖的概率也变低, +最后保证每个人中奖的概率为 \( m \over n \)。但是在实际场景中,\( m \) 个奖项可能 +不仅相同(如划分了一二三等奖),因此对于蓄水池算法的改进提出了新的要求:

+
    +
  • 将所有的奖项视为各不相同的位置,不论人数多少(当还是要保证有人来参与抽奖 \( n \gt 1\) )所有人占有特定位置的概率相同
  • +
  • 每当新来一人参与抽奖时,如果他没有中奖,可以即场告知未中1
  • +
+

算法描述与等概率性证明

+

我们分两种情况讨论:

+
    +
  • 一种是当人数不足以覆盖所有的奖项的场景( \(n \lt m \) ),
  • +
  • 另外一种是当抽奖人数远大于所有奖项加起来的数目。( \( n \gt m \))。
  • +
+

然后我们再回来看看能不能找到一种很方便的方法桥接两种情况。

+

同时,我们假设 \( m \) 个奖项两两互不相同。

+

抽奖人数不足时( \(n \lt m \) )

+

因为当人数不足时,所有参与者都能抽奖,因此我们要保证每个人获得特定奖项的概率为 \( 1 \over m \)。 +算法描述:

+
+

记 \( Choosen \) 为容量为 \( m \) 的数组, +\( Choosen[k] (1 \le k \le m) \) 表示第 k 个奖项的当前占有情况, +初始值为 \( None \),

+

\( Players \) 为参与参与抽奖的人的序列

+
    +
  1. 令 \( i := 1 \),当 \( i \le n \) 时,做如下操作: +
      +
    • 产生随机数 \( r_1 (1 \le r_1 \le i) \)
    • +
    • 如果 \( r_1 \lt i \),\( Choosen[i] := Choosen[r_1] \)
    • +
    • \( Choosen[r_1] := Players[i] \)
    • +
    • \( i := i + 1 \)
    • +
    +
  2. +
  3. 当 \( i \le m \) 时,做如下操作: +
      +
    • 产生随机数 \( r_2 (1 \le r_2 \le i) \)
    • +
    • 如果 \( r_2 \lt i \): +
        +
      • \( Choosen[i] := Choosen[r_2] \)
      • +
      • \( Choosen[r_2] := None \)
      • +
      +
    • +
    • \( i := i + 1 \)
    • +
    +
  4. +
+
+

等概率性证明

+

我们先证明,在填入中奖者的第 \( k (1 \le k \le m) \) 轮过程中,能够保证对于前 \( k \) +个奖项中的每一个奖项,每一位中奖者抽中其中第 \( i (1 \le i \le k) \) 个奖项的概率为 +\(1 \over k \),证明如下:

+

我们采用数学归纳法来证明:

+
    +
  1. 奠基:当 \( k = 1 \) 时,易知该中奖者一定会抽中第一个奖项,前一个奖项中只有第一个 +选项,所以此时每一位中奖者抽中第 \( k = 1 \) 的概率为 \( 1 = { 1 \over 1 } = { 1 \over k } \);
  2. +
  3. 归纳: +
      +
    • 假设当 \(k = j (1 \le j \lt m) \)时,每一位抽奖者抽中第 \( i (1 \le i \le j) \)的概率为 +\( 1 \over j \)
    • +
    • 当 \( k = j + 1 \), 有: +
        +
      • 第 \( j + 1 \) 位抽奖着抽中任意第 \( i' (1 \le i' \le j + 1) \) 个奖项的概率为 \( 1 \over { j + 1 } \) +(假设产生的随机数 \( r_1、r_2 \) 足够的均匀);
      • +
      • 对于前 \( j \) 位抽奖者,每一位都有 \( 1 \over { j + 1 } \),的概率将自己的奖项更换位第 \( j + 1 \)个奖项;
      • +
      • 对于前 \( j \) 位抽奖者,每一位依然占有原有第 \( i' \) 个奖项的概率为:
      • +
      +
    • +
    +
  4. +
+

\[ \begin{equation} +\begin{aligned} +P\{前 j 位抽奖者 j + 1 轮中仍然持有 i' \} & = P\{前 j 位抽奖者j轮已经持有 i' \} \cdot P\{第 j + 1 位抽奖者没有抽中 i' \} \\ +& = P\{前 j 位抽奖者j轮已经持有 i' \} \cdot (1 - P\{第 j + 1 位抽奖者抽中 i' \}) \\ +& = \frac{1}{j} \cdot (1 - \frac{1}{j+1}) \\ +& = \frac{1}{j} \cdot \frac{j}{j+1} \\ +& = \frac{1}{j + 1} \\ +& = \frac{1}{k} \\ +\end{aligned} +\label{1.1} \tag{1.1} +\end{equation} +\]

+

由上,可知每一轮迭代之后,前 \( k \) 个奖项对于已经参与的 \( k \)中奖者来说抽中的概率均等,为 \( 1 \over k \), +故到了第 \( n \) 轮操作后,我们可以通过不断填充 \( None \)值来稀释概率,最后达到 \( 1 \over m \) 的等概率性。

+

特殊地,当 \( n == m \) 时,每个抽奖者抽到特定奖项的概率也为 \(1 \over n \)。

+

抽奖人数足够多时( \(n \gt m \) )

+

类似地,当 \(n \gt m \)时,对于每一个抽奖序号 \( k \gt m \) 的抽奖者,我们生成随机数 \( r_3(1 \le r_3 \le n) \),并且在 +\( r_3 \le m \) 的时候,替换对应原本占有奖项的抽奖者;可以证明在这种情况下,能保证每个人抽到特定奖项的概率为 \(1 \over n \)2

+

整合后的算法

+
+

记 \( Choosen \) 为容量为 \( m \) 的数组, +\( Choosen[k] (1 \le k \le m) \) 表示第 \( k \) 个奖项的当前占有情况, +初始值为 \( None \),

+

\( replaced \) 为原本已经中奖,但是被人替换的抽奖者

+

\( Players \) 为参与参与抽奖的人的序列,每次只能获取一个 \( player \)

+

记 \( n := 0 \)为当前参与抽奖的人数

+
    +
  1. 在抽奖结束前,每次遇到一个新的 \( player \) 执行以下操作: +
      +
    • \( replaced := None \)
    • +
    • \( n := n + 1 \)
    • +
    • 产生随机数 \( r (1 \le r \le n) \)
    • +
    • 如果 \( r \le m \): +
        +
      • \( replaced := Choosen[r] \)
      • +
      • \( Choosen[r] := player \)
      • +
      +
    • +
    • 如果 \( r \lt n \) 并且 \( n \le m \): +
        +
      • \( Choosen[n] := replaced \)
      • +
      +
    • +
    +
  2. +
  3. 在抽奖结束时,如果 \( n \lt m \), 执行以下操作: +
      +
    • \( i := n \)
    • +
    • 当 \( i \lt m \)时,重复执行以下操作: +
        +
      • \( i := i + 1 \)
      • +
      • 产生随机数 \( r_2 (1 \le r_2 \le i) \)
      • +
      • 如果 \( r_2 \lt i \): +
          +
        • \( Choosen[i] := Choosen[r_2] \)
        • +
        • \( Choosen[r_2] := None \)
        • +
        +
      • +
      +
    • +
    +
  4. +
+
+

程序实现

+

Rust

+

作者偏好 Rust 编程语言,故使用 Rust 实现。

+

特质(trait)

+

Rust 中的特质(trait) +是其用于复用行为抽象的特性,尽管比起 Java 或 C# 的接口 (Interface)更加强大,但在此文中, +熟悉 Java/C# 的读者把特质视作接口就可以了。

+

建模与实现

+

本文使用面向对象(Object-Oriented)编程范式3来进行抽象,如下所示:

+
use rand::random;
+
+use std::fmt::Debug;
+
+trait ReservoirSampler {
+    // 每种抽样器只会在一种总体中抽样,而总体中所有个体都属于相同类型
+    type Item;
+
+    // 流式采样器无法知道总体数据有多少个样本,因此只逐个处理,并返回是否将样本纳入
+    // 样本池的结果,以及可能被替换出来的样本
+    fn sample(&mut self, it: Self::Item) -> (bool, Option<Self::Item>);
+
+    // 任意时候应当知道当前蓄水池的状态
+    fn samples(&self) -> &[Option<Self::Item>];
+}
+
+struct Lottery<P> {
+    // 记录当前参与的总人数
+    total: usize,
+
+    // 奖品的名称与人数
+    prices: Vec<Price>,
+
+    // 当前的幸运儿
+    lucky: Vec<Option<P>>,
+}
+
+#[derive(Clone, Debug)]
+struct Price {
+    name: String,
+    cap: usize,
+}
+
+impl<P> ReservoirSampler for Lottery<P> {
+    type Item = P;
+
+    fn sample(&mut self, it: Self::Item) -> (bool, Option<Self::Item>) {
+        let lucky_cap = self.lucky.capacity();
+
+        self.total += 1;
+
+        // 概率渐小的随机替换
+        let r = random::<usize>() % self.total + 1;
+        let mut replaced = None;
+        if r <= lucky_cap {
+            replaced = self.lucky[r - 1].take();
+            self.lucky[r - 1] = Some(it);
+        }
+
+        if self.total <= lucky_cap && r < self.total {
+            self.lucky[self.total - 1] = replaced.take();
+        }
+
+        (r <= lucky_cap, replaced)
+    }
+
+    fn samples(&self) -> &[Option<Self::Item>] {
+        &self.lucky[..]
+    }
+}
+
+impl<P: Debug> Lottery<P> {
+    fn release(self) -> Result<Vec<(String, Vec<P>)>, &'static str> {
+        let lucky_cap = self.lucky.capacity();
+
+        if self.lucky.len() == 0 {
+            return Err("No one attended to the lottery!");
+        }
+
+        let mut final_lucky = self.lucky.into_iter().collect::<Vec<Option<P>>>();
+        let mut i = self.total;
+        while i < lucky_cap {
+            i += 1;
+
+            // 概率渐小的随机替换
+            let r = random::<usize>() % i + 1;
+            if r <= lucky_cap {
+                final_lucky[i - 1] = final_lucky[r - 1].take();
+            }
+        }
+        println!("{:?}", final_lucky);
+
+        let mut result = Vec::with_capacity(self.prices.len());
+        let mut counted = 0;
+        for p in self.prices {
+            let mut luck = Vec::with_capacity(p.cap);
+
+            for i in 0 .. p.cap {
+                if let Some(it) = final_lucky[counted + i].take() {
+                    luck.push(it);
+                }
+            }
+
+            result.push((p.name, luck));
+            counted += p.cap;
+        }
+
+        Ok(result)
+    }
+}
+
+// 构建者模式(Builder Pattern),将所有可能的初始化行为提取到单独的构建者结构中,以保证初始化
+// 后的对象(Target)的数据可靠性。此处用以保证所有奖品都确定后才能开始抽奖
+struct LotteryBuilder {
+    prices: Vec<Price>,
+}
+
+impl LotteryBuilder {
+    fn new() -> Self {
+        LotteryBuilder {
+            prices: Vec::new(),
+        }
+    }
+
+    fn add_price(&mut self, name: &str, cap: usize) -> &mut Self {
+        self.prices.push(Price { name: name.into(), cap });
+        self
+    }
+
+    fn build<P: Clone>(&self) -> Lottery<P> {
+        let lucky_cap = self.prices.iter()
+            .map(|p| p.cap)
+            .sum::<usize>();
+
+        Lottery {
+            total: 0,
+            prices: self.prices.clone(),
+            lucky: std::vec::from_elem(Option::<P>::None, lucky_cap),
+        }
+    }
+}
+
+fn main() {
+    let v = vec![8, 1, 1, 9, 2];
+    let mut lottery = LotteryBuilder::new()
+        .add_price("一等奖", 1)
+        .add_price("二等奖", 1)
+        .add_price("三等奖", 5)
+        .build::<usize>();
+
+
+    for it in v {
+        lottery.sample(it);
+        println!("{:?}", lottery.samples());
+    }
+
+    println!("{:?}", lottery.release().unwrap());
+}
+
+

优点

+
    +
  • 流式处理,可以适应任意规模的参与人群
  • +
  • 在保证每一位抽奖者都有相同的概率获得特定奖项的同时,还能保证每一个抽奖者的获得的奖项均不相同
  • +
+

缺点

+
    +
  • 所有参与抽奖的人都必须依次经过服务器处理,因为需要获知准确的总人数来保证等概率性。 +一个改进的方法是,在人数足够多的时候,将总人数用总人数的特定数量级替代(给后续参加者的 +一点点小福利——但是因为总人数足够多,所以总体中奖概率还是很低),在客户端完成中奖的选定
  • +
  • 等概率性完全依赖随机数 r 生成。 因为奖品初始化时不需要考虑打乱顺序,因此如果在 +随机这一步被技术破解,使得抽奖者可以选择自己能获取的奖项,则会破坏公平性。改进方案是, +在 release 的时候再一次对奖品顺序进行随机的打乱。
  • +
  • 这种抽奖方式还限定了每人只能抽取一次奖品,否则会出现一个人占有多个奖项的情况。
  • +
+
2 +

可以参考博主以前的博客

+
+
3 +

作者理解的面向对象 = 对象是交互的最基本单元 + 对象通过相互发送消息进行交互。而特质/接口以及对象其他公开的方法定义了对象可以向外发送/从外接收的消息。

+
+
1 +

该条件为用以减轻开奖时发通知的压力,并非核心需求,因为对参与抽奖的玩家负责的原因,我们还是需要储存每个玩家每次的抽奖情况信息

+
+

下一步可能展开的工作

+

目前所有抽奖者都按照相等的概率抽奖,而在一些场景下可能按照一些规则给与某些抽奖者优惠 +(例如绩效越高的员工中奖概率越大),因此下一步可能考虑如何按照权重赋予每位抽奖者各自的 +中奖概率。

+

致谢

+

感谢茶壶君(@ksqsf)一语惊醒梦中人,清楚明确地表达了需求; +感谢张汉东老师 (@ZhangHanDong)老师提点了之后可以开展研究的方向; +感谢在这次讨论中提供意见的其他 Rust 社区的朋友,谢谢你们!

+

总结一次面试

+

最值得总结的三个问题

+

线程同步有哪些方法?如何用这些方法实现一个 RwLock?

+

线程同步的目的在于解决多个线程访问关键资源时的竞争状态。一个数据竞争的简单例子如下:

+
use std::thread;
+fn main() {
+  let mut s = String::from("Hello");
+
+  let thread1 = thread::spawn(|| {
+    println!("{}", s);
+  });
+
+  let thread2 = thread::spawn(|| {
+    s.push_str("World!");
+    println!("{}", s);
+  });
+
+  thread1.join();
+  thread2.join();
+}
+
+

上文的代码中 thread1 试图打印 s, 预期得到输出 Hello, 但是 thread2 却改变了 s 的内容, +那么 thread1 最终打印内容将取决于两个线程哪个先完成: 如果 thread1 先完成了,那么 +将打印 Hello; 如果 thread2 先完成了,那么将打印 HelloWorld!

+

实际上得益于 Rust 的所有权系统与生命周期(lifetime)检查,上述示例并不能编译——子线程可能 +会在主程序结束后继续运行,导致子线程捕获的 s 的引用失效;另外 thread2 直接修改了 s, +换言之只会允许 thread2 独占地持有 s 的可变引用(&mut s),而不允许其他线程持有 s +的任何引用。

+

在 Rust 编程中,主要有以下线程同步的方法:

+
    +
  • 互斥锁(Mutex) +我们可以使用互斥锁 Mutex<T> 来控制只能有单独一个线程读取/修改 +对象。通常实践是在外面加上原子引用计数 Arc 变成 Arc<Mutex<T>>,来减少 Mutex +拷贝的开销。对于多读少写的场景,可以用 RwLock 提高并发。
  • +
  • 条件变量(CondVar) +条件变量用于“阻塞”线程并使得线程在等待事件时不需要消耗 CPU 时间。通常会与放进互斥锁 +布尔型的预言值(状态值)关联使用,在状态值发生变化时通知条件变量。
  • +
  • 屏障(Barrier) +屏障用于在某个需要若干线程 都完成 前置操作后再开始计算的操作之前,让所有所需线程 +的状态都达到能开始进行计算的状态。
  • +
+

有什么问题是生存期标注无法修正的?请给出一个例子

+

这道问题最后我也并没有理解,“生命周期标注无法修正的问题”,字面意思是,即使我们按照我们 +期望的程序语义来修正了生命周期标注,这个程序仍然不能通过编译,或者再运行时仍然不能得到 +期望结果。按此描述,一个可能的例子是,我们尝试从一个较短的引用返回一个较长的引用:

+
fn longhten<'a>(s_ref: &'a str) -> &'static str {
+    s_ref
+}
+
+fn main() {
+    let s = String::from("hello");
+
+    let static_ref = longhten(&s);
+
+    println!("{}", static_ref);
+}
+
+
+

Waker 如何被唤醒? Reactor要怎么实现?

+

Reactor 作为反应器,上面同时挂载了成千上万个待唤醒的事件,这里使用了mio统一封装了操作系统的多路复用API。 +在Linux中使用的是Epoll1,在Mac中使用的则是Kqueue2

+
loop {
+    // 轮询事件是否超时
+    poll.poll(&events, timeout);
+    for event in events.iter() {
+        if (event.is_readable()) {
+            for waker in event.readers.wakers {
+                waker.wake();
+            }
+        }
+        if (event.is_writeable()) {
+            for waker in event.writers.wakers {
+                waker.wake();
+            }
+        }
+    }
+}
+
+

一面 -- 手写代码

+

1. 实现一个二分查找函数

+
#![allow(unused)]
+fn main() {
+use std::cmp::Ordering;
+
+/// 给出一个从小到大排列的数组,请实现一个函数,用二分法把指定数组 x 的位置找出来。若 x
+/// 不存在,则返回 -1. 若 x 存在多个,请返回 x 在数组中第一次出现的位置
+fn find(arr: Vec<i32>, x: i32) -> i32 {
+    let (mut left, mut right) = (0, arr.len() - 1);
+    loop {
+        if left > right {
+            return -1;
+        }
+
+        let mut mid = (left + right) / 2;
+        match arr[mid].cmp(&x) {
+            // 记得要排除已经命中的元素!
+            Ordering::Less => left = mid + 1,
+            Ordering::Greater => right = mid - 1,
+            Ordering::Equal => {
+                while mid >= 1 && arr[mid - 1] == x {
+                    mid -= 1;
+                }
+
+                return mid as i32;
+            },
+        }
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use super::*;
+
+    #[test]
+    fn should_return_minus1() {
+        let arr = vec![1, 3, 5];
+        let x = 2;
+
+        assert_eq!(find(arr, x), -1);
+    }
+
+    #[test]
+    fn should_return_mid() {
+        let arr = vec![1, 3, 5, 7, 9, 10, 10];
+        let x = 7;
+
+        assert_eq!(find(arr, x), 3);
+    }
+
+    #[test]
+    fn should_return_first() {
+        let arr = vec![1, 3, 5, 7, 9, 10, 10];
+        let x = 10;
+
+        assert_eq!(find(arr, x), 5);
+    }
+}
+}
+
+
    +
  • +

    Q: 请分析该函数的算法复杂度?

    +
      +
    • A: 时间复杂度 \( O(\log n) \),最坏情况下的事件复杂度是 \( O(n) \)
    • +
    +
  • +
  • +

    Q: 请优化这个算法?

    +
      +
    • A:一个优化方法是 插值查找法,利用如下公式自动根据查找到的元素与目标的距离来修正下一次查找 +的区间范围,提高查找速度:
    • +
    +
  • +
+

\[ mid = left + { key - arr[left] \over arr[right] - key } (right - left) \]

+

2. 镜像二叉树

+

请反转二叉树。如给出以下二叉树:

+
     1
+   /   \
+  2     3
+ / \   / \
+4   5 6   7
+
+

反转为:

+
     1
+   /   \
+  3     2
+ / \   / \
+7   6 5   4
+
+

递归解法:

+
#![allow(unused)]
+fn main() {
+use std::convert::Into;
+
+struct Node {
+    val: i32,
+    left: NodeLink,
+    right: NodeLink,
+}
+
+type NodeLink = Option<Box<Node>>;
+
+fn construct_tree() -> Box<Node> {
+    let l3left1 = Node {
+        val: 4,
+        left: None,
+        right: None,
+    };
+
+    let l3right1 = Node {
+        val: 5,
+        left: None,
+        right: None,
+    };
+
+    let l3left2 = Node {
+        val: 6,
+        left: None,
+        right: None,
+    };
+
+    let l3right2 = Node {
+        val: 7,
+        left: None,
+        right: None,
+    };
+
+    let l2left = Node {
+        val: 2,
+        left: Some(Box::new(l3left1)),
+        right: Some(Box::new(l3right1)),
+    };
+
+
+    let l2right = Node {
+        val: 3,
+        left: Some(Box::new(l3left2)),
+        right: Some(Box::new(l3right2)),
+    };
+
+    Box::new(Node {
+        val: 1,
+        left: Some(Box::new(l2left)),
+        right: Some(Box::new(l2right)),
+    })
+}
+
+fn construct_mirror() -> Box<Node> {
+    let l3left1 = Node {
+        val: 7,
+        left: None,
+        right: None,
+    };
+
+    let l3right1 = Node {
+        val: 6,
+        left: None,
+        right: None,
+    };
+
+    let l3left2 = Node {
+        val: 5,
+        left: None,
+        right: None,
+    };
+
+    let l3right2 = Node {
+        val: 4,
+        left: None,
+        right: None,
+    };
+
+    let l2left = Node {
+        val: 3,
+        left: Some(Box::new(l3left1)),
+        right: Some(Box::new(l3right1)),
+    };
+
+
+    let l2right = Node {
+        val: 2,
+        left: Some(Box::new(l3left2)),
+        right: Some(Box::new(l3right2)),
+    };
+
+    Box::new(Node {
+        val: 1,
+        left: Some(Box::new(l2left)),
+        right: Some(Box::new(l2right)),
+    })
+}
+
+impl Into<Vec<i32>> for Box<Node> {
+    fn into(mut self) -> Vec<i32> {
+        let v_left: Vec<i32>;
+        let v_right: Vec<i32>;
+        v_left = if let Some(node) = self.left.take() {
+            node.into()
+        } else {
+            Vec::new()
+        };
+
+        v_right = if let Some(node) = self.right.take() {
+            node.into()
+        } else {
+            Vec::new()
+        };
+
+       let mut v = Vec::new();
+       v.push(self.val);
+       v.extend(v_left.into_iter());
+       v.extend(v_right.into_iter());
+
+       v
+    }
+}
+
+fn mirror(root: &mut Node) {
+    let (mut tmp_left, mut tmp_right) = (NodeLink::None, NodeLink::None);
+
+    if let Some(mut node) = root.left.take() {
+        mirror(&mut node);
+        tmp_left = Some(node);
+    }
+
+    if let Some(mut node) = root.right.take() {
+        mirror(&mut node);
+        tmp_right = Some(node);
+    }
+
+    root.left = tmp_right;
+    root.right = tmp_left;
+}
+
+#[cfg(test)]
+mod test {
+    use super::*;
+
+    #[test]
+    fn should_mirrored() {
+        let mut tree = construct_tree();
+        let expect = construct_mirror();
+
+        let mirror = mirror(&mut tree);
+        assert_eq!(<Box<Node> as Into<Vec<i32>>>::into(tree), <Box<Node> as Into<Vec<i32>>>::into(expect));
+    }
+}
+}
+
+
    +
  • Q: 请优化这个算法? +
      +
    • A:如果不用递归(因为递归会加深调用栈),可以使用 广度优先搜索算法 来自根向叶 +逐层反转左右子节点的指针,并将子节点的指针放入到队列中待进行处理。
    • +
    +
  • +
+
+ + +
+

Bevy 游戏引擎编写贪吃蛇(译)

+
+

原文:https://mbuffett.com/posts/bevy-snake-tutorial/#0.3

+
+

Bevy 最近普及开来了,但是相关学习资料还是很少。这篇文章尝试提供 Bevy 官方书(The Bevy book)的下一步学习。最后产品看起来像这样:

+ +

这大约是 300 行 Rust 代码;也需要花点时间深入。如果你想快进到成品代码,请点 这里。每一个小节开头都有一份代码差异,这应该会在你不是很清晰哪里需要插入代码的时候更加清晰一点。

+

新的空的 Bevy 应用

+
+

点击查看差异

+
+

我们现在像 Bevy 官方书那样开始,整一个啥都不干的应用。运行 cargo new bevy-snake, 然后把以下代码放到你的 main.rs

+
use bevy::prelude::*;
+
+fn main() {
+    App::build().run();
+}
+
+

我们还需要在 Cargo.toml 将 Bevy 作为依赖添加,因为我(原文作者,下同)知道这个教程之后要干嘛,我们现在也提前添加 rand库吧。

+
// ...
+
+[dependencies]
+bevy = "0.3.0"
+rand = "0.7.3"
+
+

创建窗口

+
+

点击查看差异

+
+

我们现在要创建一个2D游戏,需要很多不同的系统;用来创建窗口的,用来做渲染循环的,用来处理输出的,用来处理精灵(sprites)的,等等。幸运的是,Bevy的默认插件给了我们以上所有选项:

+
fn main() {
+    App::build().add_plugins(DefaultPlugins).run();
+}
+
+

然而 Bevy 的默认插件不包括摄像机(camera),所以我们来插入一个 2D 摄像机,只要我们创建我们第一个系统就可以设置了:

+
fn setup(mut commands: Commands) {
+    commands.spawn(Camera2dComponents::default());
+}
+
+

Cammands 通常用来排列命令,来更改游戏世界与资源。在这里,我们创建一个带有 2D 摄像机组件的实体。为Bevy的魔法做点准备吧:

+
App::build()
+    .add_startup_system(setup.system()) // <--
+    .add_plugins(DefaultPlugins)
+    .run();
+
+

我们需要做的只是在我们的函数是调用 .system(),然后 Bevy 会神奇地在启动地时候调用 commands 参数。再运行一次 app, 你应该能看到一个像这样的空窗口:

+

+

开始编写一条蛇

+
+

点击查看差异

+
+

我们来写个蛇头放在窗口上吧。我们先定义几个结构体:

+
struct SnakeHead;
+struct Materials {
+    head_material: Handle<ColorMaterial>,
+}
+
+

SnakeHead 仅仅是一个空结构体,我们会把它当作一个组件来使用,它就是像某种标签,我们会放到一个实体上,之后我们能通过查询带有 SnakeHead 组件的实体来找到这个实体。像这样的空结构体在 Bevy 中是一种常见的模式,组件经常不需要他们自己的任何状态。 Materials 以后会变成一种资源,用来存储我们给蛇头使用的材质,也会用来存储蛇身和食物的材质。

+

head_material 句柄应该在游戏设置的时候就应该创建好,所以我们接下来要做的是,修改我们的 setup 函数:

+
fn setup(mut commands: Commands, mut materials: ResMut<Assets<ColorMaterial>>) {
+    commands.spawn(Camera2dComponents::default());
+    commands.insert_resource(Materials {
+        head_material: materials.add(Color::rgb(0.7, 0.7, 0.7).into()),
+    });
+}
+
+
+

注意: Bevy要求在注册系统时按照特定的顺序。命令(Commands) -> 资源(Resources) -> 组件(Components)/查询(Queries)。如果你在弄乱一个系统之后获得一个神秘的编译时错误,请检查你的顺序。

+
+

materials.add 会返回 Handle<ColorMaterial>。我们创建了使用这个新建 handle 的 Materials 结构体。之后,我们尝试访问类型为 Materials 的资源, Bevy会找到我们这个结构体。现在我们来在新的系统里创建我们的蛇头实体,然后你会看到我们如何使用前述资源的:

+
fn game_setup(mut commands: Commands, materials: Res<Materials>) {
+    commands
+        .spawn(SpriteComponents {
+            material: materials.head_material.clone(),
+            sprite: Sprite::new(Vec2::new(10.0, 10.0)),
+            ..Default::default()
+        })
+        .with(SnakeHead);
+}
+
+

现在我们有了新的系统,它会寻找类型为 Materials 的资源。它也会创建(spawn)一个新实体,带有 SpriteComponentsSnakeHead 组件。为了创建 SpriteComponents, 我们将我们之间创建的颜色的 handle 传入,并且给精灵 10x10 的大小。我们将这个系统添加到我们 app 的构建器:

+
.add_startup_system(setup.system())
+.add_startup_stage("game_setup") // <--
+.add_startup_system_to_stage("game_setup", game_setup.system()) // <--
+
+

我们需要一个新的场景而不是再一次调用 add_startup_system 的原因是,我们需要使用在 setup 函数中插入的资源。这次运行后,你应该在屏幕中央看到蛇头:

+

+

好了,可能我们叫它“蛇头”有点过了,你可以看到一个 10x10 的白色精灵。

+

移动小蛇

+
+

点击查看差异

+
+

如果小蛇不运动,那么游戏很无趣,所以我们先让蛇头动起来。我们之后再担心输入,现在我们的目标是让蛇头移动。所以我们来创建一个系统来移动所有的蛇头:

+
fn snake_movement(mut head_positions: Query<(&SnakeHead, &mut Transform)>) {
+    for (_head, mut transform) in head_positions.iter_mut() {
+        *transform.translation.y_mut() += 2.;
+    }
+}
+
+

这里有个新概念, Query 类型。我们用它来迭代所有拥有 SnakeHead 组件以及 Transform 组件的实体。我们不需要担心实际上如何创建查询类型, bevy 会帮我们创建好并用它调用我们的函数,算是 ECS 魔法的一部分。所以我们来加上这个系统, 然后看看会发生些什么:

+
.add_startup_system_to_stage("game_setup", game_setup.system())
+.add_system(snake_movement.system()) // <--
+.add_plugins(DefaultPlugins)
+
+

这是我们看到的,一头蛇移出了屏幕:

+ +

你可能再思考 Transform 组件。当我们生成 SnakeHead 时,我们并没有给它 Transform,所以我们怎么就能找到一个同事拥有 SnakeHeadTransform 组件的实体呢?实际上 SpriteComponents 是一捆组件。就 SpriteComponents 来说,它包含了 Transform 组件,以及一堆其他组件(如 Sprite, Mesh, Draw, Rotation, Sale)。

+

控制小蛇

+

我们来修改我们小蛇的移动系统,使得我们可以控制小蛇:

+
fn snake_movement(
+    keyboard_input: Res<Input<KeyCode>>,
+    mut head_positions: Query<With<SnakeHead, &mut Transform>>,
+) {
+    for mut transform in head_positions.iter_mut() {
+        if keyboard_input.pressed(KeyCode::Left) {
+            *transform.translation.x_mut() -= 2.;
+        }
+        if keyboard_input.pressed(KeyCode::Right) {
+            *transform.translation.x_mut() += 2.;
+        }
+        if keyboard_input.pressed(KeyCode::Down) {
+            *transform.translation.y_mut() -= 2.;
+        }
+        if keyboard_input.pressed(KeyCode::Up) {
+            *transform.translation.y_mut() += 2.;
+        }
+    }
+}
+
+

留意到我们的查询 Query<(&SnakeHead, &mut Transform)> 改为了 Query<With<SnakeHead, &mut Transform>>,其实当前版本没有必要更改,旧的查询依然能很好地工作。我想,第一个系统的类型签名可能简单些,但是现在我们用正确的方式编写类型。这写法更正确是因为我们其实不需要 SnakeHead 组件。所以 With 类型允许我们说,“我们需要那些有蛇头的实体,但是我不关心蛇头组件,只给我 transform 组件就好。”每个系统访问的组件越少,bevy就能并行越多的系统。例如,如果另外一个系统正在修改 SnakeHead 组件,那这个系统旧不能在用旧写法的时候并行了。

+

现在,我们能控制小蛇了,尽管它动起来不那么像蛇:

+ +

码格子

+
+

点击查看差异

+
+

到现在我们一直在用窗口的坐标,但这种方法只能在 (0, 0) 坐标在窗口正中央,并且单位是像素的时候有效。贪吃蛇游戏通常用格子,所以如果我们把我们的贪吃蛇设置成 10x10,那我们的窗口会 真的 很小。我们让日子变得轻松些吧,我们选择用我们自己的位置和尺寸。然后,我们用系统来处理变换到窗口的坐标。

+

我们先定义格子为 10x10。在程序文件开头定义如下变量:

+
const ARENA_WIDTH: u32 = 10;
+const ARENA_HEIGHT: u32 = 10;
+
+

以及我们用于处理位置/尺寸的结构体:

+
#[derive(Default, Copy, Clone, Eq, PartialEq, Hash)]
+struct Position {
+    x: i32,
+    y: i32,
+}
+
+struct Size {
+    width: f32,
+    height: f32,
+}
+impl Size {
+    pub fn square(x: f32) -> Self {
+        Self {
+            width: x,
+            height: x,
+        }
+    }
+}
+
+

相对直接地,有一个辅助方法来获取一个有相等长宽的 Size. Position 派生了一些很有用的 trait,所以我们不必不停地回顾这个结构体。 Size 可以仅仅包含一个浮点数,因为所有的对象最后都有相等的长度和宽度,但是我给它长度和宽度好像有点不对。我们现在把这些组件添加到我们生成的蛇头上:

+
commands
+    .spawn(SpriteComponents {
+        material: materials.head_material.clone(),
+        sprite: Sprite::new(Vec2::new(10.0, 10.0)),
+        ..Default::default()
+    })
+    .with(SnakeHead)
+    .with(Position { x: 3, y: 3 }) // <--
+    .with(Size::square(0.8)); // <--
+
+

这些组件暂时不做任何事情,我们现在就来将我们的尺寸映射到精灵的尺寸:

+
fn size_scaling(windows: Res<Windows>, mut q: Query<(&Size, &mut Sprite)>) {
+    let window = windows.get_primary().unwrap();
+    for (sprite_size, mut sprite) in q.iter_mut() {
+        sprite.size = Vec2::new(
+            sprite_size.width / ARENA_WIDTH as f32 * window.width() as f32,
+            sprite_size.height / ARENA_HEIGHT as f32 * window.height() as f32,
+        );
+    }
+}
+
+

这个尺寸变换逻辑是这样的:如果某个对象有一个单位格子宽度,格子宽40,然后窗口现在 400px 宽,那么它应该有10哥宽度。下面我们做位置系统:

+
fn position_translation(windows: Res<Windows>, mut q: Query<(&Position, &mut Transform)>) {
+    fn convert(pos: f32, bound_window: f32, bound_game: f32) -> f32 {
+        let tile_size = bound_window / bound_game;
+        pos / bound_game * bound_window - (bound_window / 2.) + (tile_size / 2.)
+    }
+    let window = windows.get_primary().unwrap();
+    for (pos, mut transform) in q.iter_mut() {
+        transform.translation = Vec3::new(
+            convert(pos.x as f32, window.width() as f32, ARENA_WIDTH as f32),
+            convert(pos.y as f32, window.height() as f32, ARENA_HEIGHT as f32),
+            0.0,
+        );
+    }
+}
+
+ +

位置变换:如果项目的 X 坐标在我们的系统中是 5,宽度是 10,并且窗口宽度是200,那么坐标应该是 5/10 * 200 - 200 / 2。我们减去一半的窗口宽度,因为我们的做消息是从左下角开始,然后替换到正中央。然后我们再加上半个格子,因为我们想要我们精灵的左下角对齐格子的左下角,而不是精灵中心对齐。

+

然后我们把这些系统加到我们的应用构建器上:

+
.add_system(snake_movement.system())
+.add_system(position_translation.system()) <--
+.add_system(size_scaling.system()) <--
+.add_plugins(DefaultPlugins)
+.run();
+
+
+

注意: 现在最明显的问题是小蛇被压扁了。另外一个问题是我们破环了我们的输入处理。我们先修复输入处理,然后我们得记得回来处理我们被压扁的小蛇,把它恢复原状。

+
+

+

使用我们的格子

+
+

点击查看差异

+
+

我们现在配置好了格子坐标,现在我们需要更新我们的 snake_movement 系统。之前我们使用 Transform 的地方,现在替换成 Position

+
fn snake_movement(
+    keyboard_input: Res<Input<KeyCode>>,
+    mut head_positions: Query<With<SnakeHead, &mut Position>>,
+) {
+    for mut pos in head_positions.iter_mut() {
+        if keyboard_input.pressed(KeyCode::Left) {
+            pos.x -= 1;
+        }
+        if keyboard_input.pressed(KeyCode::Right) {
+            pos.x += 1;
+        }
+        if keyboard_input.pressed(KeyCode::Down) {
+            pos.y -= 1;
+        }
+        if keyboard_input.pressed(KeyCode::Up) {
+            pos.y += 1;
+        }
+    }
+}
+
+ +

调整窗口大小1

+
+

点击查看差异

+
+

我们上一步中的小蛇被压扁了,是因为默认的窗口尺寸并不是方形的,然而我们的格子是,所以我们每个格坐标会宽度长于高度。我们修复它最简单的方法,是在构建 app 的时候创建一个 WindowDescriptor 资源:

+
    App::build()
+        .add_resource(WindowDescriptor { // <--
+            title: "Snake!".to_string(), // <--
+            width: 200,                 // <--
+            height: 200,                // <--
+            ..Default::default()         // <--
+        })
+        .add_startup_system(setup.system())
+
+

同时,我们改一下背景颜色,插入这个 use 语句来引入 ClearColor 结构体:

+
use bevy::render::pass::ClearColor;
+
+

然后在 app 构建器增加资源:

+
.add_resource(ClearColor(Color::rgb(0.04, 0.04, 0.04)))
+
+
1 +

原文中这里的规格是 2000,但是 2000 的规则放 10x10 显然太大了, 这里改成 200

+
+ +

生成食物

+

现在我们的小蛇可以到处移动了,该喂点东西给它了。现在我们给 Materials 加一个 food_materials 字段:

+
struct Materials {
+    head_material: Handle<ColorMaterial>,
+    food_material: Handle<ColorMaterial>, // <--
+}
+
+

然后把这个新材质加到我们的 setup 函数里:

+
commands.insert_resource(Materials {
+    head_material: materials.add(Color::rgb(0.7, 0.7, 0.7).into()),
+    food_material: materials.add(Color::rgb(1.0, 0.0, 1.0).into()), // <--
+});
+
+

然后我们需要 Duration 给要创建的定时器使用,而且我们还需要 random 来随机分配食物的位置。先在程序里引入这些:

+
use rand::prelude::random;
+use std::time::Duration;
+
+

然后我们因素两个新结构体: Food 组件让我们知道哪个实体是食物,以及一个定时制造食物的定时器:

+
struct Food;
+
+struct FoodSpawnTimer(Timer);
+impl Default for FoodSpawnTimer {
+    fn default() -> Self {
+        Self(Timer::new(Duration::from_millis(1000), true))
+    }
+}
+
+

至于实现 Default 的原因,会在我解释下面的系统的时候说明:

+
fn food_spawner(
+    mut commands: Commands,
+    materials: Res<Materials>,
+    time: Res<Time>,
+    mut timer: Local<FoodSpawnTimer>,
+) {
+    timer.0.tick(time.delta_seconds);
+    if timer.0.finished {
+        commands
+            .spawn(SpriteComponents {
+                material: materials.food_material.clone(),
+                ..Default::default()
+            })
+            .with(Food)
+            .with(Position {
+                x: (random::<f32>() * ARENA_WIDTH as f32) as i32,
+                y: (random::<f32>() * ARENA_HEIGHT as f32) as i32,
+            })
+            .with(Size::square(0.8));
+    }
+}
+
+

我们引入了局部资源概念,具体而言是 timer 参数。 Bevy 会看到这个参数并且实例化一个 FoodSpawnTimer 类型的值,用的是我们的 Default 实现。这会在这个系统第一次运行是发生,之后这个系统会一直重用相同的定时器。像这样使用局部资源要比手动注册资源更贴近工程化。这个定时器会一直重复,所以我们只需要调用 tick 函数,然后无论这个系统在定时器完成后什么时候跑,我们就随机创建一些食物。

+

你可能知道下一步是什么了,把这个系统加到应用构建器上:

+
.add_system(food_spawner.system())
+
+

现在我们的程序看起来像这样:

+ +

更像蛇的移动

+
+

点击查看差异

+
+

我们现在准备定时触发小蛇移动。具体说来,我们想小蛇一直在移动,无论我们是否按下按键;并且我们想要它每隔 X 秒移动一次,而不是每一帧都移动。我们会改动几个地方,所以如果你不太清楚要改动哪里,查看这一小节的差异吧。

+

首先,我们需要加一个方向枚举:

+
#[derive(PartialEq, Copy, Clone)]
+enum Direction {
+    Left,
+    Up,
+    Right,
+    Down,
+}
+
+impl Direction {
+    fn opposite(self) -> Self {
+        match self {
+            Self::Left => Self::Right,
+            Self::Right => Self::Left,
+            Self::Up => Self::Down,
+            Self::Down => Self::Up,
+        }
+    }
+}
+
+

然后把这个方向枚举加到我们的 SnakeHead 结构体,使得它知道应该要往哪里移动:

+
struct SnakeHead {
+    direction: Direction,
+}
+
+

我们也得在实例化 SnakeHead 组件的时候给定初始方向,例如我们让它一开始往上走:

+
.with(SnakeHead {
+    direction: Direction::Up,
+})
+
+

小蛇通常移动不是很流畅,是一种一步步来的行动。就行我们生成食物的时候,我们需要使用定时器来让系统没每隔 X秒/毫秒才跑一次。我们需要创建一个结构体来持有定时器:

+
struct SnakeMoveTimer(Timer);
+
+

然后我们把它当成资源加到我们的 app 构建器:

+
.add_resource(SnakeMoveTimer(Timer::new(
+    Duration::from_millis(150. as u64),
+    true,
+)))
+
+

我们之所以不把这个定时器像生成食物的时候把定时器看成局部资源,是因为我们将会在几个系统里用上它,所以我帮你节约了一些重构的工作。因为我们需要在几个系统里使用它,我们需要创建一个新系统来触发这个定时器:

+
fn snake_timer(time: Res<Time>, mut snake_timer: ResMut<SnakeMoveTimer>) {
+    snake_timer.0.tick(time.delta_seconds);
+}
+
+

我们也可以把这段触发逻辑直接放到 snake_movement 系统里,但是我比较喜欢整洁地吧它放到一个单独的系统中,因为这个定时器会用在几个地方。我们把这个系统也加到 app上:

+
.add_system(snake_timer.system())
+
+

现在我们可以做方向逻辑的核心部分,也就是 snake_movement 系统,以下是更新后的版本:

+
fn snake_movement(
+    keyboard_input: Res<Input<KeyCode>>,
+    snake_timer: ResMut<SnakeMoveTimer>,
+    mut heads: Query<(Entity, &mut SnakeHead)>,
+    mut positions: Query<&mut Position>,
+) {
+    if let Some((head_entity, mut head)) = heads.iter_mut().next() {
+        let mut head_pos = positions.get_mut(head_entity).unwrap();
+        let dir: Direction = if keyboard_input.pressed(KeyCode::Left) {
+            Direction::Left
+        } else if keyboard_input.pressed(KeyCode::Down) {
+            Direction::Down
+        } else if keyboard_input.pressed(KeyCode::Up) {
+            Direction::Up
+        } else if keyboard_input.pressed(KeyCode::Right) {
+            Direction::Right
+        } else {
+            head.direction
+        };
+        if dir != head.direction.opposite() {
+            head.direction = dir;
+        }
+        if !snake_timer.0.finished {
+            return;
+        }
+        match &head.direction {
+            Direction::Left => {
+                head_pos.x -= 1;
+            }
+            Direction::Right => {
+                head_pos.x += 1;
+            }
+            Direction::Up => {
+                head_pos.y += 1;
+            }
+            Direction::Down => {
+                head_pos.y -= 1;
+            }
+        };
+    }
+}
+
+

这里没有什么新概念,仅仅是游戏逻辑。你可能在想为什么我们需要获取拥有 SankeHead 组件的 Entity, 然后用另外一个独立的查询来获取位置, 而不是用像 Query<Entity, &SnakeHead, &mut Position> 这样的参数。原因在于,我们之后可能需要其他实体的位置,而分开两个查询访问相同的组件是不会允许放在 Bevy app 构建器上的。这样改了之后,你会获得一个蛇头移动的稍微……像蛇一样:

+ +

加个尾巴

+
+

点击查看差异

+
+

小蛇的尾巴有点复杂。对于每蛇尾的分段,我们需要知道它下一步需要到哪里。我们准备这样实现:将这些分段放到 Vec,然后存储为资源。这样,当我们更新分段的位置时,我们能够迭代所有的分段并且设置每个分段的位置为前一个分段的位置。

+

我们加一个 segment_material 字段到我们趁手的 Materials 结构体:

+
struct Materials {
+    head_material: Handle<ColorMaterial>,
+    segment_material: Handle<ColorMaterial>, // <--
+    food_material: Handle<ColorMaterial>,
+}
+
+

老调重弹,把 segment_material 加到 setup 中:

+
commands.insert_resource(Materials {
+    head_material: materials.add(Color::rgb(0.7, 0.7, 0.7).into()),
+    segment_material: materials.add(Color::rgb(0.3, 0.3, 0.3).into()), // <--
+    food_material: materials.add(Color::rgb(1.0, 0.0, 1.0).into()),
+});
+
+

然后一个给蛇身分段的组件:

+
struct SnakeSegment;
+
+

然后我们再加上我们说到的,用来存储分段列表的资源:

+
#[derive(Default)]
+struct SnakeSegments(Vec<Entity>);
+
+

再把它作为资源加到我们的 app 上:

+
.add_resource(SnakeSegments::default())
+
+

我们我们需要从几个地方生成分段(当你吃食物或者你初始化小蛇的时候),我们需要先创建一个辅助函数:

+
fn spawn_segment(
+    commands: &mut Commands,
+    material: &Handle<ColorMaterial>,
+    position: Position,
+) -> Entity {
+    commands
+        .spawn(SpriteComponents {
+            material: material.clone(),
+            ..SpriteComponents::default()
+        })
+        .with(SnakeSegment)
+        .with(position)
+        .with(Size::square(0.65))
+        .current_entity()
+        .unwrap()
+}
+
+

这看上去非常像我们生成 SnakeHead 的函数,但是替换了 SnakeHead 组件,我们用的是 SnakeSegment 组件。这里要说的新知识点,就是我们最后通过 current_entity 函数,获取了生成的 Entity (其实只是个 id),然后将它返回给调用者以便使用它。现在,我们需要修改我们的游戏配置函数。并非只是生成一个蛇头,它现在要生成一个蛇身的分段:

+
fn spawn_snake(
+    mut commands: Commands,
+    materials: Res<Materials>,
+    mut segments: ResMut<SnakeSegments>,
+) {
+    segments.0 = vec![
+        commands
+            .spawn(SpriteComponents {
+                material: materials.head_material.clone(),
+                ..Default::default()
+            })
+            .with(SnakeHead {
+                direction: Direction::Up,
+            })
+            .with(SnakeSegment)
+            .with(Position { x: 3, y: 3 })
+            .with(Size::square(0.8))
+            .current_entity()
+            .unwrap(),
+        spawn_segment(
+            &mut commands,
+            &materials.segment_material,
+            Position { x: 3, y: 2 },
+        ),
+    ];
+}
+
+

我们第一个分段是头部,现在我们多加了一个 with(SnakeSegment)。第二个分段来自我们的 spawn_segment 函数。我们现在得到了一条小小的尾巴:

+ +

让尾巴跟着小蛇活动

+
+

点击查看差异

+
+

正如我记得那样,蛇尾没有脱离蛇头,是贪吃蛇游戏中重要的一部分。我们来看看,我们可以怎么修改 snake_movement 函数,来更接近原汁原味的游戏。首先要做的事把 SnakeSegments 资源到 snake_movement 函数上:

+
fn snake_movement(
+    keyboard_input: Res<Input<KeyCode>>,
+    snake_timer: ResMut<SnakeMoveTimer>,
+    segments: ResMut<SnakeSegments>, // <--
+    mut heads: Query<(Entity, &mut SnakeHead)>,
+    mut positions: Query<&mut Position>,
+
+

现在,直接在最前面的 if let 后面,我们加上所有分段的位置(当然,不要忘了蛇头的位置):

+
let segment_positions = segments
+    .0
+    .iter()
+    .map(|e| *positions.get_mut(*e).unwrap())
+    .collect::<Vec<Position>>();
+
+

然后我们要做的是在 if let 的末尾迭代蛇身分段(跳过蛇头,因为我们已经通过用户输入更新了位置),然后让每个分段的位置都变成前一个分段的。例如,第一个蛇身分段设置为当前蛇头(更新前)的位置,第二段的设置为第一段的。

+
segment_positions
+    .iter()
+    .zip(segments.0.iter().skip(1))
+    .for_each(|(pos, segment)| {
+        *positions.get_mut(*segment).unwrap() = *pos;
+    });
+
+

现在我们的游戏看起来应该像这样:

+ +

小蛇成长

+
+

点击查看差异

+
+

小蛇已经饿坏了。我们现在需要家一个系统来让小蛇猎食:

+
fn snake_eating(
+    mut commands: Commands,
+    snake_timer: ResMut<SnakeMoveTimer>,
+    mut growth_events: ResMut<Events<GrowthEvent>>,
+    food_positions: Query<With<Food, (Entity, &Position)>>,
+    head_positions: Query<With<SnakeHead, &Position>>,
+) {
+    if !snake_timer.0.finished {
+        return;
+    }
+    for head_pos in head_positions.iter() {
+        for (ent, food_pos) in food_positions.iter() {
+            if food_pos == head_pos {
+                commands.despawn(ent);
+                growth_events.send(GrowthEvent);
+            }
+        }
+    }
+}
+
+

只是迭代所有的食物位置,来看他们是不是和蛇头共享一个位置,如果是这样,我们就用 despawn 者趁手的函数移除食物,然后触发一个 GrowthEvent。我们来创建这个结构体:

+
struct GrowthEvent;
+
+

使用事件是个新概念。你可以在系统间发送或接受事件,他们可以是任意类型的结构体,使得你可以在事件里包括任何你需要发送的数据。例如,你可能有一个系统发送跳跃事件,然后一个独立的系统来处理他们。在我们的这个案例中,我们需要一个系统来发送成长事件,以及一个成长系统来处理它们。你需要注册事件,就像我们对资源和系统做的那样:

+
.add_event::<GrowthEvent>()
+
+

然后在这里我们也加上 snake_eating 系统:

+
.add_system(snake_eating.system())
+
+

现在小蛇应该能够猎食了。但是小蛇现在就像个黑洞,吃多少也不长大。在思考成长这事时,需要注意我们需要知道最后的分段移动前在哪里,因为那里是新的分段成长的位置。现在我们来创建一个新资源:

+
#[derive(Default)]
+struct LastTailPosition(Option<Position>);
+
+

然后在 app 构建器上:

+
.add_resource(LastTailPosition::default())
+
+

我们也要对 snake_movement 系统做一点小修改,来更新 LastTailPosition 资源。首先先把这个资源加到参数中:

+
fn snake_movement(
+    // ...
+    mut last_tail_position: ResMut<LastTailPosition>, // <--
+    // ...
+
+

然后就是给这个资源分配最后的一个分段的位置。这段代码放在我们迭代过了 segment_positions 之后:

+
last_tail_position.0 = Some(*segment_positions.last().unwrap()); // <--
+
+

之后,小蛇成长的函数就很清晰了:

+
fn snake_growth(
+    mut commands: Commands,
+    last_tail_position: Res<LastTailPosition>,
+    growth_events: Res<Events<GrowthEvent>>,
+    mut segments: ResMut<SnakeSegments>,
+    mut growth_reader: Local<EventReader<GrowthEvent>>,
+    materials: Res<Materials>,
+) {
+    if growth_reader.iter(&growth_events).next().is_some() {
+        segments.0.push(spawn_segment(
+            &mut commands,
+            &materials.segment_material,
+            last_tail_position.0.unwrap(),
+        ));
+    }
+}
+
+

以及追加系统:

+
.add_system(snake_growth.system())
+
+ +

撞墙(或者咬尾巴)

+
+

点击查看差异

+
+

现在我们来增加撞墙和咬尾巴来触发游戏结束(game over)。我们使用一个新事件,就像我们在“小蛇成长小节”中那样:

+
struct GameOverEvent;
+
+

并把它注册到 app 构建器上:

+
.add_event::<GameOverEvent>()
+
+

在我们的 snake_movement 系统中,我们想要访问 “游戏结束” 事件,使得我们能够发送事件:

+
fn snake_movement(
+    // ...
+    mut game_over_events: ResMut<Events<GameOverEvent>>, // <--
+    // ...
+) {
+
+

我们先关注在撞墙事件上面。把这部分代码放到 match &head.direction { 后面:

+
if head_pos.x < 0
+    || head_pos.y < 0
+    || head_pos.x as u32 >= ARENA_WIDTH
+    || head_pos.y as u32 >= ARENA_HEIGHT
+{
+    game_over_events.send(GameOverEvent);
+}
+
+

好了,现在我们的 snake_movement 系统可以发送 “游戏结束” 事件了,我们再来创建一个系统来监听这些事件:

+
fn game_over(
+    mut commands: Commands,
+    mut reader: Local<EventReader<GameOverEvent>>,
+    game_over_events: Res<Events<GameOverEvent>>,
+    materials: Res<Materials>,
+    segments_res: ResMut<SnakeSegments>,
+    food: Query<With<Food, Entity>>,
+    segments: Query<With<SnakeSegment, Entity>>,
+) {
+    if reader.iter(&game_over_events).next().is_some() {
+        for ent in food.iter().chain(segments.iter()) {
+            commands.despawn(ent);
+        }
+        spawn_snake(commands, materials, segments_res);
+    }
+}
+
+

这里有个很酷的点: 我们可以直接使用 spawn_snake 函数,现在它既是一个系统,也是一个辅助函数了。

+

最后一个修改点,就是我们得让小蛇咬到尾巴的时候也会触发 “游戏结束” 事件。在 snake_movement 系统中,在我们检查完边界的部分后添加:

+
if segment_positions.contains(&head_pos) {
+    game_over_events.send(GameOverEvent);
+}
+
+

最后,我们的成果:

+ +

Rust 安全应用开发51条

+
+

本文摘自法国国家网络安全局(ASSNI)的《用 Rust 开发安全应用的编程规则》(PROGRAMMING RULES TO DEVELOPSECURE APPLICATIONS WITH RUST

+
+
    +
  1. 要使用 stable 编译工具链
  2. +
  3. 要在 cargo 配置文件中将重要变量保持为默认值
  4. +
  5. 要在运行 cargo 时保持编译环境变量为默认值
  6. +
  7. 要周期地使用 linter
  8. +
  9. 要使用 Rust 格式器(rustfmt)
  10. +
  11. 要人工检查自动修复
  12. +
  13. 要检查依赖版本是否过期(cargo-outdated)
  14. +
  15. 要检查依赖的安全脆弱性(vulnerabilities)(cargo-audit)
  16. +
  17. 要遵循命名转换
  18. +
  19. 不要使用 unsafe
  20. +
  21. 要用合适的算术操作来处理潜在的溢出
  22. +
  23. 推荐实现包含了所有可能错误的自定义错误类型
  24. +
  25. 推荐使用 ? 操作符且不使用 try!
  26. +
  27. 不要使用能导致 panic! 的函数
  28. +
  29. 要测试数组索引使用是否正确,或者使用 get 方法
  30. +
  31. 要在 FFI 中正确地处理 panic!
  32. +
  33. 不要使用 forget
  34. +
  35. 推荐使用 clippy 检查 forget 的使用
  36. +
  37. 不要泄露内存
  38. +
  39. 要释放包裹在 ManaullyDrop 里的值
  40. +
  41. 总是调用 into_rawed 值对应的 from_raw 函数
  42. +
  43. 不要使用未初始化内存
  44. +
  45. 使用完敏感数据后要将内存清零
  46. +
  47. 推荐校验 Drop 实现
  48. +
  49. 不要再 Drop 实现内部恐慌(panic)
  50. +
  51. 不允许 Drop 的循环引用
  52. +
  53. 推荐不依赖 Drop 来保证安全
  54. +
  55. 推荐校验 SendSync 实现
  56. +
  57. 要遵循标准库比较特质(trait)的不变之处
  58. +
  59. 推荐使用标准库比较特质的默认实现
  60. +
  61. 推荐尽可能派生(derive)比较特质
  62. +
  63. 要在 FFI 中只使用 C 兼容类型
  64. +
  65. 在 FFI 边界要使用兼容性的类型
  66. +
  67. 推荐使用绑定自动生成工具
  68. +
  69. 在绑定到平台依赖类型时,要使用可移植别名 c_*
  70. +
  71. 推荐在 Rust 中检查外部类型
  72. +
  73. 推荐指针类型而不是引用类型1
  74. +
  75. 不要使用未检查的外部引用
  76. +
  77. 检查外部指针
  78. +
  79. 要标记 FFI 中的函数指针类型为 externunsafe
  80. +
  81. 检查外部函数指针
  82. +
  83. 建议不在 FFI 边界使用不美容的 Rust enum 类型
  84. +
  85. 建议为外部不透明类型使用专门的 Rust 类型
  86. +
  87. 推荐使用不完整的 C/C++ struct 指针来使得类型不透明
  88. +
  89. 不要在 FFI 边界使用实现了 Drop 的类型
  90. +
  91. 要确保在 FFI 中清除数据所有权
  92. +
  93. 推荐将外部数据包裹在可释放内存的包装类型
  94. +
  95. 要在 FFI 中 正确地处理 panic!
  96. +
  97. 推荐为外部库提供安全的包装
  98. +
  99. 推荐只暴露专门的 C 兼容 API
  100. +
+

在天河二号上配置 Rust 运行环境

+
+

受朋友委托,需要帮忙在“天河二号”超级计算机上配置 Rust 编程语言运行环境,并配置安装 rust-overlaps

+
+

阅读须知

+

本文将不涉及:

+ +

通过 Rust 独立包安装适合 天河二号的 Rust 运行环境

+
    +
  1. ssh 远程登录到天河二号1: +
    $ ssh -i${YOUR_CERTIFICATE_ID} -P${SSH_PORT} ${YOUR_USERNAME}@server.ip.in.vpn
    +
    +
  2. +
  3. 获取超算的服务器平台架构: +
    [you@tainhe2-H ~]$ uname -r
    +
    +
  4. +
  5. 了解平台架构后,获取对应平台的Rust 独立安装包, 并上传至超算。此处以x86_64架构为例: +
    $ scp -i${YOUR_CERTIFICATE_ID} -P${SSH_PORT} rust-1.44.0-x86_64-unknown-linux-gnu.tar.gz you@server.ip.in.vpn:~
    +
    +
  6. +
  7. 解压安装压缩包: +
    [you@tainhe2-H ~]$ tar -zxvf rust-1.44.0-x86_64-unknown-linux-gnu.tar.gz
    +
    +
  8. +
  9. 切换到解压缩目录,并执行安装命令: +
    [you@tainhe2-H ~]$ cd rust-1.44.0-x86_64-unknown-linux-gnu
    +[you@tainhe2-H rust-1.44.0-x86_64-unknown-linux-gnu]$ ./install.sh --prefix=~/rust --disable-ldconfig --verbose
    +
    +此命令会将 Rust 安装在 ~/rust 文件夹中,rust 的 可执行文件将会放在 ~/rust/bin文件夹中。
  10. +
  11. 编辑~/.bashrc, 增加下面这一行配置: +
    export PATH=$HOME/rust/bin:$PATH
    +
    +
  12. +
  13. 使~/.bashrc生效: +
    [you@tainhe2-H ~]$ source ~/.bashrc
    +
    +
  14. +
  15. 检查 Rust 是否成功安装: +
    [you@tainhe2-H ~]$ cargo --version
    +cargo 1.44.0 (05d080faa 2020-05-06)
    +
    +
  16. +
+

离线安装 rust-overlaps

+
    +
  1. 在本地联网环境拷贝源代码: +
    git clone https://github.com/sirkibsirkib/rust-overlaps.git
    +
    +
  2. +
  3. 修复源码的 Cargo.tomlversion2: +
    version = "1.1.0"
    +
    +
  4. +
  5. 在代码仓库目录下执行 cargo vendor,获取依赖的源码3: +
    rust-overlaps$ cargo vendor --respect-source-config
    +
    +下载好的依赖将会存放到 vendor文件夹中。
  6. +
  7. rust-overlaps 文件夹中添加 .cargo/config 文件,以便在超算的离线环境中使用本地缓存好的依赖源码进行编译: +
    [source.crates-io]
    +replace-with = "vendored-sources"
    +
    +[source.vendored-sources]
    +directory = "vendor"
    +
    +
  8. +
  9. 将源码文件夹打包成 .zip 包,然后上传到超算: +
    $ scp -i${YOUR_CERTIFICATE_ID} -P${SSH_PORT} rust-overlaps.zip you@server.ip.in.vpn:~
    +
    +
  10. +
  11. 在超算中解压: +
    [you@tainhe2-H ~]$ unzip rust-overlaps.zip
    +
    +
  12. +
  13. 离线安装3: +
    [you@tainhe2-H ~]$ cd rust-overlaps
    +[you@tainhe2-H rust-overlaps]$ cargo install --path . --offline
    +
    +
  14. +
  15. 检查是否安装成功: +
    [you@tainhe2-H ~]$ rust-overlaps --version
    +ASPOPsolver 1.0
    +
    +
  16. +
+

小结

+

当我看到 rust-overlaps 已经超过三年没有更新之后,我就觉得很可能不能够成功编译——但是 Rust 从来没有让我失望 —— 在本文中,我们使用的是最新稳定版的 Rust 1.44, 然而编译一个三年的旧库一次就可以编译成功了。同样,得益于 Rust 以 crate 为单位的并行与增量编译,让编译命令中断后可以继续执行而不需从头编译。这个故事告诉我们,充分吸收现代学术成果的工具比起偏旧的工具对于效率提高有重要影响!

+
1 +

ssh登陆前还需登录VPN环境,账号密码为管理员提供的账号密码。

+
+
2 +

Rust仓库的版本号遵循语义化版本,因此必须为x.y.z的形式。更多参见sirkibsirkib/rust-overlaps#2

+
+
3 +

cargo编译中断,可以重新运行命令继续安装,直到安装完成。

+
+

设计模式在 Rust 中的实践

+

[设计模式] 在 Rust 中的应用与其在其他语言中的应用有很多不同之处,本系列为个人在使用 Rust 编程的时候遇到的一些设计模式,并结合自己的思考对 Rust 编写过程中设计模式有特点地优化。

+

特别提醒: 没有银弹!没有银弹!没有银弹! 所有的设计都是为了解决特定场景下的问题,脱离场景扯设计模式都是耍流氓!

+

构建器(Builder)模式

+

示例

+

通常在 Rust 中的实现是通过 不断重建 Builder 来构造最后的类型:

+
struct Counter {
+    counted1: usize,
+    counted2: usize,
+    done: bool,
+}
+
+struct CounterBuilder {
+    counted1: usize,
+    counted2: usize,
+}
+
+impl CounterBuilder {
+    // 构建器需要有默认的参数配置,然后从默认配置触发进行构建。
+    // 不适用 #[derive(std::default::Default)],因为默认配置可能不一样
+    fn default() -> Self {
+        CounterBuiler {
+            counted1: 5,
+            counted2: 0,
+        }
+    }
+
+    // 属性定制方法。消耗原本的构建器,修改属性后重新生成新构建器
+    fn set_counted1(self, cnt: usize) -> Self {
+        self.counted1 = cnt;
+        self
+    }
+
+    fn set_counted2(self, cnt: usize) -> Self {
+        self.counted2 = cnt;
+        self
+    }
+
+    // 最后通过 `build` 方法生成所需类型
+    fn build(self) -> Counter {
+        Counter {
+            counted1: self.counted1,
+            counted2: self.counted2,
+            done: false,
+        }
+    }
+}
+
+

个人实践

+

在设置属性方法的时候,通常的实现是通过消耗原本的构造器后生成新构造器,这使得如果配置构造器的过程不能连续调用属性设置方法时,必须重新捕获构造器:

+
let mut builder = CounterBuilder::default();
+
+// ... 进行一些计算,获得需要配置的值
+let cnt1 = operations();
+
+builder = builder.set_counted1(cnt);
+
+// ... 进行一些计算,获得需要配置的值
+let cnt2 = operations();
+
+builder = builder.set_counted(cnt2);
+
+

以上代码通常出现在需要流计算并及时记录参数配置的时候。并且,如果构造器被更大型的数据结构持有时,消耗并重新构建构造器可能会对性能有点影响。因此在博主个人实现时通常采取传递&mut self 引用的方法来实现属性设置方法:

+
    // ...
+    // 属性定制方法。消耗原本的构建器,修改属性后重新生成新构建器
+    fn set_counted1(&mut self, cnt: usize) -> &mut Self {
+        self.counted1 = cnt;
+        self
+    }
+
+    fn set_counted2(&mut self, cnt: usize) -> &mut Self {
+        self.counted2 = cnt;
+        self
+    }
+
+// ...
+
+

改成如上形式的函数签名,即可 灵活构造 目标结构:

+
let mut builder = CounterBuilder::default();
+
+// ... 进行一些计算,获得需要配置的值
+let cnt1 = operations();
+
+builder.set_counted1(cnt);
+
+// ... 进行一些计算,获得需要配置的值
+let cnt2 = operations();
+
+builder.set_counted(cnt2);
+
+// ... 可能还要等待别的操作完成后再进行构建
+
+let counter = builder.build();
+
+

更好用的工具

+

本文在微信公众号发布之后,微信粉丝 @福糙·仁波切 推荐了 dtolnay/proc-macro-workshop 中的 derive_builder,能够更快地实现自定义的Builder。例如,上文中的示例使用该库可以大大减少代码:

+
use derive_builder::Builder;
+
+struct Counter {
+    #[builder(default = "5")]
+    counted1: usize,
+
+    #[builder(default)]
+    counted2: usize,
+
+    #[builder(default)]
+    done: bool,
+}
+
+
+

甚至可以快速自定义自己的 setter:

+
use derive_builder::Builder;
+
+struct Counter {
+    #[builder(default = "5")]
+    counted1: usize,
+
+    #[builder(default)]
+    counted2: usize,
+
+    #[builder(default)]
+    done: bool,
+}
+
+impl CounterBuilder {
+    fn set_counted2(&mut self, cnt: usize) -> &mut Self {
+        // 注意生成的构造器的字段为 `Option`
+        self.counted2 = Some(if cnt > 100 { 100 } else { cnt });
+        self
+    }
+}
+
+fn main() {
+    let mut builder = CounterBuilder::default();
+
+    builder.set_counted2(123);
+
+    let counter = builder.build();
+
+    println!("{:?}", counter);
+}
+
+
+

为什么使用构造器模式

+
    +
  • 构造过程可控。通常实现构造器模式的时候,我们会将构造器所需要配置的属性设置为私有1,并且只能通过我们提供的属性设置方法进行设置,使得构造过程可控。另外,可以通过属性设置方法提前恐慌(panic)来阻止生成无效对象。
  • +
  • 设置方法职责专一。属性设置方法 职责专一,只会负责设置一种属性,只有在该属性的设置规则改变时,相应的属性设置方法才需要进行修改;
  • +
  • 构造灵活。多个属性设置方法可以自由的组合在一起,也可以分步组合构造。
  • +
  • 可批量构造。我们除了使用消耗性的 build(self) 方法,也可以使用非消耗性的 fn build(&self) 方法,使得构造器可以多次复用。
  • +
  • 符合开闭原则。当某一属性的设置方法内部实现发生变化的时候,不影响其他属性的设置方式;而新增属性及其设置方法时,可以通过链式调用很方便地增加新属性的设置。
  • +
+

为什么不使用构造器模式

+

构造器模式由于有以下缺点而在部分场景中不适用:

+
    +
  • 在构造完成前无法使用被构造对象。在构造完成之前,构造器并不生成被构造对象,因此在整个构造设置完成之前,无法使用被构造对象。
  • +
  • 构造器与被构造对象使用相同的属性设置方法,造成代码重复并无法复用。考虑需要只通过属性设置方法来修改对象的场景,当被构造对象在使用过程中需要频繁设置属性,那么就需要编写对应的属性设置方法;而如果还使用构造器进行对象构造,那么属性设置方法就会重复,并且可能造成构造器与被构造对象的属性设置行为不一致的问题2
  • +
+
1 +

Rust 语言中默认语言项(Item)的可见性都是私有的,如需公开语言项给其他模块使用,需要使用 pub 关键字放开。 +2: 一个绕开的行为不一致问题的方法是将属性设置规则抽取为静态函数,但仍然无法避免过度封装的问题。不过,可以将过度封装的事情交给过程宏等自动代码生成手段,例如文中举例的 derive_builder

+
+

抽象工厂(Abstract Factory)模式

+

场景需求

+

我们在设计游戏的时候,经常会遇到设计类似关卡的需求,例如 RPG 游戏中的副本(dungeon),每种副本都循序类似的模式、类似的行为,但是采用资源不同,行为的细节可能有所差异,为了方便我们新增新的副本,同时使得原有副本的修改尽可能少影响游戏的运行机制(所谓符合开闭原则),我们可以采用抽象工厂模式来统一管理游戏副本的生成。

+

场景设计

+

我们先设计一个简单的副本模式,包含了一个 Boss 和若干小怪,小怪数量在范围内随机波动。小怪和 Boss 都拥有血量,但是只有Boss 能攻击。我们把这个模式定义为结构:

+
struct Dungeon {
+    boss: Box<dyn Boss>,
+    monsters: Vec<Box<dyn Monster>>,
+}
+
+trait Monster {
+    // 获知当前血量
+    fn life(&self) -> u64;
+
+    // 怪物血量可以因受到攻击降低,也可自行回复
+    fn change_life(&mut self, diff: i32);
+}
+
+trait Boss: Monster {
+    // Boss 能攻击玩家(Hero)
+    fn attack(&self target: &mut Box<dyn Hero>);
+}
+
+

上面在运行时已经不关心具体是什么副本(当然,副本的元数据可以添加为 Dungeon 结构的字段),我们只需要知道它有一个 Boss 和若干 Monster

+

然后,我们建立抽象工厂:

+
use rand::random;
+trait DungeonFactory {
+    fn create_boss(&self) -> Box<dyn Boss>
+    fn create_mosters(&self, amount: usize) -> Vec<Box<dyn Monster>>
+
+    fn generate_dungeon(&self) -> Dungeon {
+        Dungeon {
+            boss: self.create_boss(),
+            monsters: self.create_monsters(random::<u8>() % 2 + 2), // 2 ~ 3 只小怪
+        }
+    }
+}
+
+

然后,我们创建新手副本的具体工厂:

+
struct NewbieDungeonFactory;
+
+impl DungeonFactory for NewBieDungeonFactory {
+    fn create_boss(&self) -> Box<dyn Boss> {
+        NewBieBoss::new()
+    }
+
+    // 新手副本甚至没有小怪,
+    fn create_mosters(&self, amount: usize) -> Vec<Box<dyn Monster>> {
+        Vec::new()
+    }
+
+    // generate_dungeon 已有默认实现,不需要修改。
+}
+
+// 新手副本,boss极弱
+struct NewbieBoss {
+    max_life: u8,
+    life: u8,
+    attack: u8,
+}
+
+impl NewbieBoss {
+    fn new() -> Self {
+        NewBieBoss {
+            max_life: 5,
+            life: 5,
+            attack: 1,
+        }
+    }
+}
+
+impl Monster for NewBieBoss {
+    fn life(&self) -> u64 {
+        self.life as u64
+    }
+
+    fn change_life(&mut self, diff: i32) {
+        self.life = match self.life + diff {
+            n if n < 0 => 0,
+            n if n > self.max_life => self.max_life
+            n => n
+        };
+    }
+}
+
+impl Boss for NewbieBoss {
+    fn attack(&self, target: &mut Box<dyn Hero>) {
+        target.attacked_wtih(self.attack);
+    }
+}
+
+

上面的工厂看上去好像很多内容,主要是因为 Boss特质需要一个新载体 NewBieBoss,同时实现 Boss 载体的 NewBieBoss 也需要实现 Monster 特质(继承关系组合化), 这部分的代码作为示例让读者理解设定的 Dungeon 模式的行为。

+

接下来,我们可以新增一个新副本,新副本的小怪和 Boss 是新手副本的 NewbieBoss,但是小怪已经不能攻击玩家了(通过动态分发为 dyn Monster 对象来屏蔽 Boss 的攻击行为:

+
struct JuniorDungeonFactory;
+
+impl DungeonFactory for JuniorDungeonFactory {
+    fn create_boss(&self) -> Box<dyn Boss> {
+        NewBieBoss::new()
+    }
+
+    // 新手副本甚至没有小怪,
+    fn create_mosters(&self, amount: usize) -> Vec<Box<dyn Monster>> {
+        vec![NewBieBoss::new(); amount]
+    }
+
+    // generate_dungeon 已有默认实现,不需要修改。
+}
+
+

我们可以看到,抽象工厂模式很方便地为我们重用了已有资源并确保副本的行为效果。

+

为什么使用抽象工厂模式

+
    +
  • 模式统一。通过抽象工厂可以快速地制作符合相同模式,但是资源细节稍有差异的工厂,生成具有同样属性的产品。
  • +
  • 职责专一。每个具体工厂只有一个原因需要被修改——该工厂对应的副本细节变化了。
  • +
  • 符合开闭原则。因为我们统一出了抽象工厂的接口,当我们修改具体工厂的具体实现细节时,并不会影响到接口调用;而新增具体工厂时只需要提供对应的接口,也可以方便地接入副本生成系统。
  • +
+

Rust 镜像

+
+

由于在 Rust 群经常有新人重复提问诸如 “Rust 下载很慢,怎么办?” “Rust 怎么安装更快” 一类的提问,因此整理国内常用的用于加速的镜像和反向代理。

+
+

阅读须知

+

本文将不涉及:

+ +

使用国内镜像加速更新 Rustup 工具链

+

我们需要指定 RUSTUP_DIST_SERVER(默认指向 https://static.rust-lang.org)和 RUSTUP_UPDATE_ROOT (默认指向https://static.rust-lang.org/rustup),这两个网站均在中国大陆境外,因此在中国大陆访问会很慢,需要配置成境内的镜像。

+

以下 RUSTUP_DIST_SERVERRUSTUP_UPDATE_ROOT 可以组合使用。

+
# 清华大学
+RUSTUP_DIST_SERVER=https://mirrors.tuna.tsinghua.edu.cn/rustup
+
+# 中国科学技术大学
+RUSTUP_DIST_SERVER=https://mirrors.ustc.edu.cn/rust-static
+RUSTUP_UPDATE_ROOT=https://mirrors.ustc.edu.cn/rust-static/rustup
+
+# 上海交通大学
+RUSTUP_DIST_SERVER=https://mirrors.sjtug.sjtu.edu.cn/rust-static/
+
+

+

使用国内镜像加速更新 crate 拉取

+

将如下配置写入 $HOME/.cargo/config 文件1 2

+
# 放到 `$HOME/.cargo/config` 文件中
+[source.crates-io]
+registry = "https://github.com/rust-lang/crates.io-index"
+
+# 替换成你偏好的镜像源
+replace-with = 'sjtu'
+
+# 清华大学
+[source.tuna]
+registry = "https://mirrors.tuna.tsinghua.edu.cn/git/crates.io-index.git"
+
+# 中国科学技术大学
+[source.ustc]
+registry = "git://mirrors.ustc.edu.cn/crates.io-index"
+
+# 上海交通大学
+[source.sjtu]
+registry = "https://mirrors.sjtug.sjtu.edu.cn/git/crates.io-index"
+
+# rustcc社区 - 已失效!
+# [source.rustcc]
+# registry = "https://code.aliyun.com/rustcc/crates.io-index.git"
+
+# rustcc 1号源
+[source.rustcc]
+registry="git://crates.rustcc.com/crates.io-index"
+
+# rustcc 2号源
+[source.rustcc2]
+registry="git://crates.rustcc.cn/crates.io-index"
+
+

参考资料:

+

Rustup 镜像安装帮助 - 清华大学

+

Rust Toolchain 反向代理使用帮助 - 中国科学技术大学

+

国内Rust库文件镜像 - rustcc

+

在中国大陆cargo命令速度很慢怎么办?

+
1 +

如遇到 invalid UTF-8 的问题请去掉文中中文注释

+
+
2 +

建议不要用Windows默认的记事本编辑

+
+

在 WSL 中学习 Rust FFI

+
+

博主最近从新学习 Rust FFI 的使用,但是手头上没有可用的 Linux 环境(Windows 编译c太麻烦了),于是就尝试着使用 WSL来搭建 Rust 环境和简易的 c 编译环境,并记录下中间遇到的一些坑。感谢 Unsafe Rust 群群友 @框框 对本文的首发赞助!感谢 Rust 深水群 @栗子 的 gcc 指导!

+
+

阅读须知

+

阅读本文,你可以知道:

+
    +
  • 一些配置 WSL 全局变量的技巧
  • +
  • 快速配置 Rust 编译运行环境
  • +
  • 简单的 gcc 编译技巧
  • +
+

但是,本文不涉及:

+ +

WSL Rust 环境搭建

+

由于 WSL 是新装的,没有 Rust 和 gcc/g++ 环境,因此需要安装:

+
sudo apt install gcc -y
+
+# 官方脚本
+curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
+
+ +

但是由于在国内访问 Rust 官方网站会很慢,因此设置镜像到 Windows 环境变量中:

+
RUSTUP_DIST_SERVER=https://mirrors.ustc.edu.cn/rust-static
+RUSTUP_UPDATE_ROOT=https://mirrors.ustc.edu.cn/rust-static/rustup
+
+

然后,使用 WSLENV环境变量将上述变量共享到 WSL 中:

+
WSLENV=RUSTUP_DIST_SERVER:RUSTUP_UPDATE_ROOT
+
+

然后重启 WSL 终端,重新执行 Rust 一键脚本。

+

以下两个项目均来自 《Rust编程之道》一书,源代码仓库在这里

+

Rust 调用 C/C++

+

Rust 调用 C/C++ 代码可以使用 cc crate 配合 build.rs 预先编译好 C/C++ 的程序提供给 Rust 调用。

+

首先,创建一个 binary 项目:

+
cargo new --bin ffi_learn
+
+

项目目录结构如下:

+
cpp_src
+    |-- sorting.h
+    |-- sorting.cpp
+src
+    |-- main.rs
+Cargo.toml
+build.rs
+
+

然后编写 sorting.hsorting.cpp:

+
// sorting.h
+#ifndef __SORTING_H__
+#define __SORTING_H__ "sorting.h"
+#include <iostream>
+#include <functional>
+#include <algorithm>
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+void interop_sort(int[], size_t);
+
+#ifdef __cplusplus
+}
+#endif
+#endif
+
+
// sorting.cpp
+#include "sorting.h"
+
+void interop_sort(int numbers[], size_t size) {
+    int* start = &numbers[0];
+    int* end = &numbers[0] + size;
+
+    std::sort(start, end, [](int x, int y) { return x > y; });
+}
+
+

然后给 Cargo.toml[build-dependecies] 加上 cc crate 依赖:

+
# Cargo.toml
+# 其他配置
+
+[build-dependencies]
+cc = "1"
+
+

接着,我们通过 cc 调用对应平台的c/c++编译器,因为我们这个项目是 WSL,所以和调用我们刚安装的 gcc:

+
// build.rs
+// Rust 2018 不需要 extern crate 语句
+
+fn main() {
+    cc::Build::new()
+        .cpp(true)
+        .warnings(true)
+        .flag("-Wall")
+        .flag("-std=c++14")
+        .flag("-c")
+        .file("cpp_src/sorting.cpp")
+        .compile("sorting");    // sorting.so
+}
+
+

接着,我们在 Rust 主程序中,通过 extern 块引入sorting.cpp中的interop_sort函数,并调用它:

+
// main.rs
+#[link(name = "sorting", kind = "static")]
+extern "C" {
+    fn interop_sort(arr: *mut i32, n: u32);
+}
+
+pub fn sort_from_cpp(arr: &mut [i32]) {
+    unsafe {
+        // 传入必然有效的数组引用,并通过传入数组的长度来保证不会出现越界访问,从而保证函数内存安全
+        interop_sort(arr as *mut [i32] as *mut i32, arr.len() as u32);
+    }
+}
+
+fn main() {
+    let mut my_arr: [i32; 10] = [10, 42, -9, 12, 8, 25, 7, 13, 55, -1];
+    println!("Before sorting...");
+    println!("{:?}\n", my_arr);
+
+    sort_from_cpp(&mut my_arr);
+
+    println!("After sorting...");
+    println!("{:?}\n", my_arr);
+}
+
+

然后执行调用:

+
$ cargo run
+   Compiling ffi_learning v0.1.0 (/mnt/c/Users/huangjj27/Documents/codes/ffi_learning)
+warning: `extern` block uses type `[i32]`, which is not FFI-safe
+ --> src/main.rs:3:26
+  |
+3 |     fn interop_sort(arr: &[i32], n: u32);
+  |                          ^^^^^^ not FFI-safe
+  |
+  = note: `#[warn(improper_ctypes)]` on by default
+  = help: consider using a raw pointer instead
+  = note: slices have no C equivalent
+
+    Finished dev [unoptimized + debuginfo] target(s) in 4.71s
+     Running `target/debug/ffi_learn`
+Before sorting...
+[10, 42, -9, 12, 8, 25, 7, 13, 55, -1]
+
+After sorting...
+[55, 42, 25, 13, 12, 10, 8, 7, -1, -9]
+
+

我们看到,该函数提示我们 C 中并没有等价于 Rust slice 的类型,原因在于如果我们传递 slice,那么在 C/C++ 中就很容易访问超过数组长度的内存,造成内存不安全问题。但是,我们在 Rust 调用的时候,通过同时传入数组 arr 的长度 arr.len(), 来保证函数不会访问未经授权的内存。不过在实践中,应该划分模块,只允许确认过 内存安全的 safe Rust 功能跨越模块调用。

+

在 C/C++ 中调用 Rust

+

接下来我们反过来互操作。项目结构如下:

+
c_src
+    |-- main.c
+src
+    |-- lib.rs
+    |-- callrust.h
+Cargo.toml
+makefile
+
+

然后配置 Rust 生成两种库——静态库(staticlib)和c动态库(cdylib):

+
# Cargo.toml
+# ...
+
+[lib]
+name = "callrust"   # 链接库名字
+crate-type = ["staticlib", "cdylib"]
+
+

然后添加我们的 Rust 函数:

+
#![allow(unused)]
+fn main() {
+// lib.rs
+
+// `#[no_mangle]` 关闭混淆功能以让 C 程序找到调用的函数
+// `extern` 默认导出为 C ABI
+#[no_mangle]
+pub extern fn print_hello_from_rust() {
+    println!("Hello from rust");
+}
+}
+
+

当然,为了给 C 调用我们还需要编写一个头文件:

+
// callrust.h
+void print_hello_from_rust();
+
+

在我们的 main.c 中库并调用:

+
// main.c
+#include "callrust.h"
+#include <stdio.h>
+#include <stdint.h>
+#include <inttypes.h>
+
+int main(void) {
+    print_hello_from_rust();
+}
+
+ +

编写 makefile,先调度cargo 编译出我们需要的 Rust 库(动态或链接),然后再运行:

+
GCC_BIN ?= $(shell which gcc)
+CARGO_BIN ?= $(shell which cargo)
+
+# 动态链接 libcallrust.so
+share: clean cargo
+	mkdir cbin
+	$(GCC_BIN) -o ./cbin/main ./c_src/main.c -I./src -L./target/debug -lcallrust
+
+	# 注意动态链接再运行时也需要再次指定 `.so` 文件所在目录,否则会报错找不到!
+	LD_LIBRARY_PATH=./target/debug ./cbin/main
+
+# 静态链接 libcallrust.a
+static: clean cargo
+	mkdir cbin
+
+	# libcallrust.a 缺少了一些pthread, dl类函数,需要链接进来
+	$(GCC_BIN) -o ./cbin/main ./c_src/main.c -I./src ./target/debug/libcallrust.a -lpthread -ldl
+	./cbin/main
+
+clean:
+	$(CARGO_BIN) clean
+	rm -rf ./cbin
+
+cargo:
+	$(CARGO_BIN) build
+
+

小结

+

本文通过给出两个简单的示例来展示 Rust 通过 FFI 功能与 C/C++ 生态进行交互的能力, 并且指出几个在实践过程中容易浪费时间的坑:

+
    +
  1. WSL的环境变量不生效 -> 使用 WSLENV 变量从 Windows 引入使用。
  2. +
  3. make share 的时候提示 libcallrust.so 找不到 -> 需要在运行时指定 LD_LIBRARY_PATH 变量,引入我们编译的 libcallrust.so 路径。
  4. +
  5. make static的时候遇到了pthread_* dy*系列函数未定义问题 -> 通过动态链接系统库来支持运行。
  6. +
+

WASM(Web Assembly)尽管是为了提高网页中性能敏感模块表现而提出的字节码标准, 但是WASM却不仅能用在浏览器(broswer)中, 也可以用在其他环境中. 在这些环境中, 我们则需要支持WASI(WebAssembly System Interface, WASM系统接口)的runtime来执行我们编译运行的wasm模块.

+

WASI探索(一) -- WASI简介与Wasmtime配置

+

什么是WASI?

+

WASI1是一个新的API体系, 由Wasmtime项目设计, 目的是为WASM设计一套引擎无关(engine-indepent), 面向非Web系统(non-Web system-oriented)的API标准. 目前, WASI核心API(WASI Core)在做覆盖文件, 网络等等模块的API, 但这些实现都是刚刚开始实现, 离实用还是有很长路要走.

+

关于WASM runtime

+

在了解了WASI之后, 博主最后选定两个WASM运行时进行探索: WASMER 与 Wasmtime. 这两款运行时都号称开始支持了WASI标准, 但博主使用rust-wasi-tutorial对两款运行时进行试验后, 发现WASMER对于文件读取还是有些问题, 而Wasmtime则是通过了规格测试(基于specs testsuite), 因此本文接下来着重于Wasmtime的配置介绍.

+

Wasmtime与rust环境配置

+

由于目前Wasmtime与WASMER均只支持Unix-like环境, 接下来楼主将演示如何在WSL(Ubuntu 18.04)下配置Wasmtime. 而在目前比较方便生成wasm的编程语言中, 博主选择使用自带wasi目标的rust编程语言, 可以"零代价"配置wasm相关工具链.

+

配置rust

+
    +
  1. 下载并安装rustup: curl https://sh.rustup.rs -sSf | sh, 安装时使用默认 stable-x86_64-unknown-linux-gnu工具链, 后面我们还会自行添加用于编译wasm的nightly工具链.
  2. +
  3. 为cargo配置ustc反代, 提高crates(rust库)下载速度2
  4. +
  5. 安装rustfmt: rustup component add rustfmt --toolchain stable-x86_64-unknown-linux-gnu. Wasmtime的test脚本需要用到该组件.
  6. +
  7. 安装rust nightly工具链: rustup toolchain add nightly-x86_64-unknown-linux-gnu. 当前rust的WASI目标还在开发中, 尚未稳定3.
  8. +
  9. 安装rust WASI目标: rustup target add wasm32-unknown-wasi3.
  10. +
+

配置Wasmtime

+
    +
  1. 安装cmake与clang: sudo apt install cmake clang, 用于编译Wasmtime. Wasmtime目前尚未有正式发布版本, 故需要我们自行编译.
  2. +
  3. 拷贝Wasmtime源码: git clone --recursive git@github.com:CraneStation/wasmtime.git ~/wasmtime.
  4. +
  5. 切换到Wasm源码目录: cd ~/wasmtime
  6. +
  7. 执行测试脚本: ./scripts/test-all.sh. 当脚本执行完毕并通过测试后, 说明wasmtime已经正常编译并且能在当前WSL环境下正常工作, 可以使用生成的wasmtime可执行文件.
  8. +
  9. 将生成的wasmtime拷贝到/usr/bin目录中: cp ~/wasmtime/target/release/wasmtime /usr/bin, 以便在整个WSL环境中任意目录执行wasmtime. wasmtime是个单文件(stand alone)运行时.
  10. +
  11. 执行wasmtime --help命令, 确认wasmtime成功安装.
  12. +
+

试验

+

GitHub上面已经有了比较简单的试验, 大家按照上面的说明 +去试验即可. 下一篇文章, 博主将会把猜数字编译成WASI目标并执行, 同时会尝试把一些常用的库尝试编译, 来探究 +当前社区对WASI支持的程度.

+ + +
3 +

截至2020年1月19日,WASI目标已经稳定并重命名为wasm32-wasi

+
+

WASI探索(二) -- WASI版猜数字

+
+

猜数字作为入门Rust时第一次编写并具有实际功能的程序,适合让读者快速掌握rust的基本概念。同时,为了让程序更加有趣,博主在原本的猜数字程序上添加了日志和从运行时参数传递游戏难度的功能。此外,由于博主偏好改变,本文还会涉及到另外一款WASI运行时Wasmer,以及他们为了丰富WASI生态而推出的wasm包管理器wapm。

+
+

阅读须知

+

学习外部资料更有助于读者了解相关生态,因此本文将不赘述:

+ +

而阅读本文,你将了解:

+
    +
  • 如何用日志debug的一些原则
  • +
  • 一个简单的配置文件的设计
  • +
  • 读者对Wasmer的一些浅薄看法
  • +
+

原版猜数字

+

我们从官方书拷贝了一份猜数字程序:

+
// main.rs
+use std::io;
+use std::cmp::Ordering;
+use rand::Rng;
+
+fn main() {
+    println!("Guess the number!");
+
+    let secret_number = rand::thread_rng().gen_range(1, 101);
+
+    loop {
+        println!("Please input your guess.");
+
+        let mut guess = String::new();
+
+        io::stdin().read_line(&mut guess)
+            .expect("Failed to read line");
+
+        let guess: u32 = match guess.trim().parse() {
+            Ok(num) => num,
+            Err(_) => continue,
+        };
+
+        println!("You guessed: {}", guess);
+
+        match guess.cmp(&secret_number) {
+            Ordering::Less => println!("Too small!"),
+            Ordering::Greater => println!("Too big!"),
+            Ordering::Equal => {
+                println!("You win!");
+                break;
+            }
+        }
+    }
+}
+
+
# Cargo.toml
+[package]
+name = "guessing_game"
+version = "0.1.0"
+authors = ["Your Name <you@example.com>"]
+edition = "2018"
+
+[dependencies]
+rand = "0.3.14"
+
+

一次游戏只猜一个数

+

我们可以看到,这个程序每次运行,只能猜一个数字,如果要继续玩就只能重新启动。但是博主想让这个游戏,能在一次运行时 +可以生成不同难度关卡,因此首先我们将“猜一个数字”逻辑抽取成可复用函数

+
#![allow(unused)]
+fn main() {
+// main.rs
+fn guess_a_number() {
+    println!("Guess the number!");
+
+    let secret_number = rand::thread_rng().gen_range(1, 101);
+
+    loop {
+        println!("Please input your guess.");
+
+        let mut guess_str = String::new();
+
+        io::stdin().read_line(&mut guess_str)
+            .expect("Failed to read line");
+
+        let guess: u32 = match guess_str.trim().parse() {
+            Ok(num) => num,
+            Err(_) => continue,
+        };
+
+        println!("You guessed: {}", guess);
+
+        match guess.cmp(&secret_number) {
+            Ordering::Less => println!("Too small!"),
+            Ordering::Greater => println!("Too big!"),
+            Ordering::Equal => {
+                println!("You win!");
+                break;
+            }
+        }
+    }
+}
+}
+
+

再将配置文件修改一下:

+
# guess_wasi/Cargo.toml
+[package]
+name = "guess_wasi"
+version = "0.1.0"
+authors = ["huangjj27 <huangjj.27@qq.com>"]
+edition = "2018"
+
+[dependencies]
+rand = "0.7"
+
+

此外,猜数字游戏的难度取决于随机生成数字的范围, 为了生成不同的难度关卡,我们需要guess_a_number接受一组控制 +生成数字范围的参数:

+
#![allow(unused)]
+fn main() {
+// main.rs
+/// 生成熟悉范围的下界(lower bound,lb)与上界(higher bound,hb)在主函数中读取配置文件得到
+fn guess_a_number((lb, hb): (u32, u32)) {
+    println!("Guess the number!");
+
+    let secret_number = rand::thread_rng().gen_range(lb, hb + 1);
+
+    // ...
+}
+}
+
+

这里在传入参数时,直接解构元组, 这样后面就可以直接使用传入的上界与下界来控制生成数范围

+

然后,博主发现, 原版猜数字如果解析数字错误的话会直接跳过,博主觉得这里应该至少提醒一下用户输入错误了:

+
#![allow(unused)]
+fn main() {
+// main.rs
+fn guess_a_number((lb, hb): (u32, u32)) {
+    // ...
+        let guess: u32 = match guess_str.trim().parse() {
+            Ok(num) => num,
+            Err(_) => {
+                println!("Input not a number! please input a number");
+                continue;
+            },
+        };
+    // ...
+}
+}
+
+

加上log追踪生成的数据情况

+

使用log去追踪数据与可能产生bug的代码有以下好处:

+
    +
  • 了解运行时所关注的数据情况, 方便定位bug
  • +
  • 清晰地知道实际运行流程是否如期望那样执行
  • +
  • 即便使用release版目标, 仍然可以获得需要的分析信息
  • +
  • 区分产生信息的层级,以便将精力集中在优先需要处理的信息中
  • +
+

回到猜数字游戏上,博主想要知道每一次游戏中知道生成的secret_number是多少, 并且根据运行时输入的日志层级的参数 +决定是否显示这个数字,需求相对简单,因此使用rust生态中比较常用的log crateenv_log crate。在Cargo.toml中加入两个新依赖:

+
# guess_wasi/Cargo.toml
+
+# ...
+
+[dependencies]
+rand = "0.7"
+
+# 总是使用最新的log与env_log
+log = "*"
+env_logger = "*"
+
+

加入追踪日志代码:

+
// main.rs
+use log::{trace, debug};
+
+fn main() {
+    // 别忘了初始化日志生成器, 才能获取日志!
+    env_log::init();
+    guess_a_number((1, 100));
+}
+
+fn guess_a_number((lb, hb): (u32, u32)) {
+    println!("Guess the number!");
+
+    let secret_number = rand::thread_rng().gen_range(lb, hb + 1);
+    trace!("secret number: {}", secret);
+
+    loop {
+        println!("Please input your guess.");
+
+        let mut guess_str = String::new();
+        io::stdin().read_line(&mut guess_str)
+            .expect("Failed to read line");
+        debug!("scaned string: {:?}", guess_str);
+
+        // ...
+    }
+}
+
+

向高难度挑战!

+

现在我们来到最后了一个需求:通过运行时参数来给每次游戏输入多个游戏难度,这个难度由随机数生成的范围决定-- 随机数 +生成的范围越大,一次猜中这个数的概率就越小。为方便地写出输入参数的命令,我们需要引入structopt库(crate), +最后获得类似--levels=10 100 1000这样的参数输入方式, 参数中每个数字表示每次生成随机数的生成范围上界。

+

配置文件追加:

+
# guess_wasi/Cargo.toml
+
+# ...
+
+[dependencies]
+rand = "0.7"
+
+# 总是使用最新的log与env_log
+log = "*"
+env_logger = "*"
+
+structopt = "*"
+
+

编写参数代码。

+
// main.rs
+
+// ...
+use structopt::StructOpt;
+
+// 定义参数只需要把他们的名字和类型写在一个参数结构体中即可!
+#[derive(StructOpt)]
+#[structopt(name="guess_wasi")]
+struct Opt {
+    #[structopt(long="levels")]
+    levels: Vec<u32>,
+}
+
+fn main() {
+    env_logger::init();
+
+    // 获取并访问levels参数, 只需要访问参数结构体的对应成员即可, 细节处理可以方便地交给库执行!
+    let opt = Opt::from_args();
+    for &lv in &opt.levels {
+        println!("given number range 0~{}", lv);
+        guess_a_number((0, lv));
+    }
+}
+
+

完整代码

+
[package]
+name = "guess_wasi"
+version = "0.1.0"
+authors = ["huangjj27 <huangjj.27@qq.com>"]
+edition = "2018"
+
+[dependencies]
+rand = "0.7"
+
+# 总是使用最新的log与env_log
+log = "*"
+env_logger = "*"
+
+structopt = "*"
+
+
// guess_wasi/main.rs
+use std::io;
+use std::cmp::Ordering;
+use rand::Rng;
+use log::{debug, trace};
+
+use structopt::StructOpt;
+
+// 定义参数只需要把他们的名字和类型写在一个参数结构体中即可!
+#[derive(StructOpt)]
+#[structopt(name="guess_wasi")]
+struct Opt {
+    #[structopt(long="levels")]
+    levels: Vec<u32>,
+}
+
+fn main() {
+    env_logger::init();
+
+    // 获取并访问levels参数, 只需要访问参数结构体的对应成员即可, 细节处理可以方便地交给库执行!
+    let opt = Opt::from_args();
+    for &lv in &opt.levels {
+        println!("given number range 0~{}", lv);
+        guess_a_number((0, lv));
+    }
+}
+
+// 一场游戏有多个难度,我们每个难度只猜一个数字,然后变难
+fn guess_a_number((lb, hb): (u32, u32)) {
+    let secret = rand::thread_rng().gen_range(lb, hb + 1);
+    trace!("secret number: {}", secret);
+
+    loop {
+        println!("Please input your guess.");
+
+        let mut guess_str = String::new();
+        io::stdin().read_line(&mut guess_str)
+            .expect("Failed to read line");
+        debug!("scaned string: {:?}", guess_str);
+
+        let guess: u32 = match guess_str.trim().parse() {
+            Ok(num) => num,
+            Err(_) => {
+                println!("Input not a number! please input a number");
+                continue;
+            },
+        };
+
+        println!("You guessed: {}", guess);
+
+        match guess.cmp(&secret) {
+            Ordering::Less => println!("too small!"),
+            Ordering::Greater => println!("too big!"),
+            Ordering::Equal => {
+                println!("You get it!");
+                break;
+            }
+        }
+    }
+}
+
+

读到这里,读者可以发现前文根本没涉及到WASI,甚至没有涉及WASM。这因为WASI作为应用与运行时交互的接口,被rust编译器封装成为编译目标,读者只需要编译到对应目标即可让自己的程序在对应平台上运行. 这是Rust编程语言现代化与工程学的体现: +一般应用研发工程师可以通过使用已经适配所需平台的底层库(这些底层库通常已经针对所有支持平台做了最优化适配),就能让自己的应用支持对应的平台而无需重新编写针对某平台的特化版本源码!

+

是时候编译成WASI目标了

+

我们还需要添加对应的编译目标:

+
rustup target add wasm32-wasi
+
+

编译到wasm32-wasi目标上:

+
$ cargo build --target=wasm32-wasi --release
+   Compiling proc-macro2 v1.0.18
+   Compiling version_check v0.9.2
+   Compiling unicode-xid v0.2.0
+   Compiling syn v1.0.30
+   Compiling cfg-if v0.1.10
+   Compiling memchr v2.3.3
+   Compiling getrandom v0.1.14
+   Compiling wasi v0.9.0+wasi-snapshot-preview1
+   Compiling lazy_static v1.4.0
+   Compiling bitflags v1.2.1
+   Compiling atty v0.2.14
+   Compiling unicode-width v0.1.7
+   Compiling unicode-segmentation v1.6.0
+   Compiling log v0.4.8
+   Compiling quick-error v1.2.3
+   Compiling ansi_term v0.11.0
+   Compiling ppv-lite86 v0.2.8
+   Compiling regex-syntax v0.6.18
+   Compiling strsim v0.8.0
+   Compiling vec_map v0.8.2
+   Compiling termcolor v1.1.0
+   Compiling thread_local v1.0.1
+   Compiling textwrap v0.11.0
+   Compiling proc-macro-error-attr v1.0.2
+   Compiling proc-macro-error v1.0.2
+   Compiling humantime v1.3.0
+   Compiling heck v0.3.1
+   Compiling quote v1.0.7
+   Compiling rand_core v0.5.1
+   Compiling clap v2.33.1
+   Compiling regex v1.3.9
+   Compiling rand_chacha v0.2.2
+   Compiling env_logger v0.7.1
+   Compiling syn-mid v0.5.0
+   Compiling rand v0.7.3
+   Compiling structopt-derive v0.4.7
+   Compiling structopt v0.3.14
+   Compiling guess_wasi v0.1.0 (C:\Users\huangjj27\Documents\codes\huangjj27.github.io\code\guess_wasi)
+    Finished release [optimized] target(s) in 4m 58s
+
+

现在,我们来运行一下程序吧:

+
$ wasmer --version
+wasmer 0.13.1
+$ wasmer run .\target\wasm32-wasi\release\guess.wasm --env RUST_LOG=trace -- --levels 10 100 1000
+given number range 0~10
+[2020-06-09T14:55:58Z TRACE guess] secret number: 10
+Please input your guess.
+5
+[2020-06-09T14:56:02Z DEBUG guess] scaned string: "5\r\n"
+You guessed: 5
+too small!
+Please input your guess.
+8
+[2020-06-09T14:56:04Z DEBUG guess] scaned string: "8\r\n"
+You guessed: 8
+too small!
+Please input your guess.
+9
+[2020-06-09T14:56:07Z DEBUG guess] scaned string: "9\r\n"
+You guessed: 9
+too small!
+Please input your guess.
+10
+[2020-06-09T14:56:09Z DEBUG guess] scaned string: "10\r\n"
+You guessed: 10
+You get it!
+given number range 0~100
+[2020-06-09T14:56:09Z TRACE guess] secret number: 60
+Please input your guess.
+60
+[2020-06-09T14:56:25Z DEBUG guess] scaned string: "60\r\n"
+You guessed: 60
+You get it!
+given number range 0~1000
+[2020-06-09T14:56:25Z TRACE guess] secret number: 715
+Please input your guess.
+300
+[2020-06-09T14:56:32Z DEBUG guess] scaned string: "300\r\n"
+You guessed: 300
+too small!
+Please input your guess.
+720
+[2020-06-09T14:56:38Z DEBUG guess] scaned string: "720\r\n"
+You guessed: 720
+too big!
+Please input your guess.
+716
+[2020-06-09T14:56:41Z DEBUG guess] scaned string: "716\r\n"
+You guessed: 716
+too big!
+Please input your guess.
+714
+[2020-06-09T14:56:46Z DEBUG guess] scaned string: "714\r\n"
+You guessed: 714
+too small!
+Please input your guess.
+715
+[2020-06-09T14:56:48Z DEBUG guess] scaned string: "715\r\n"
+You guessed: 715
+You get it!
+$
+
+

调试后,确认我们的程序可以正常执行了, 去掉--env RUST_LOG=trace参数,享受自己制作的这个小游戏吧!

+ +

Wasmer与Wapm

+

Wasmer可以是说在WASI生态中响应速度仅次于Mozilla的组织,他们号称打造了 +一款可以让代码“一次构建,处处运行”(Build Once, Run Anywhere.)的运行时环境,该环境可以运行ECMAScripten标准与 +WASI标准的wasm栈机码。并且方便为wasm代码分发,该组织开发了类似于nodejs生态中npm的包管理工具wapm,这样用户就可以 +很轻松地发布自己的程序,以及利用他人的程序了--这促进了WASM生态的发展,同时作为生态底层的领导者,Wasmer也将拥有 +更多发言权。

+

作为边缘人士(稍微知道WASM生态但没很深入了解),博主看到这项目背后的布局很像上世纪Sun公司的Java和JVM(尽管WASM并不是Wasmer的发明,但这样反而不必为WASM这样可以作为主流编程语言编译目标工具投入过多精力宣传,可以集中精力去优化wasmer与wapm;同时因为wasmer是使用MIT协议授权,不会产生类似OracleJDK专利权所属的问题,相信随着生态的进一步发展,在虚拟机运行时领域会逐步替代JVM成为主流,届时将解放程序员更多生产力 -- 不必要求掌握Java而是通过自己熟悉的编程语言(c/c++/rust/python/...)通过统一的标准相互调用(进一步微型化的微服务)。

+

而这个在服务器/PC桌面应用占主导地位的标准,就是WASI。

+

初等数论自我探索

+

本系列文章以解决哥德巴赫猜想为目的(证明/证伪),把思路限制在初等数论(个人水平有限,理解不了更高级的工具),进行思考与尝试,本意是打发自己零散的通勤时间,走到哪算到哪。

+

该系列文章的最初设想是有一天突然就想到,关于一个偶数 \( 2n (n \in Z^+) \)的素数相加的分拆,一定是关于整数 \( n \) 对称的,即若 \( 2n = p_1 + p_2 \),其中 \( p_1 \), \( p_2 \) 为质数(\(p_1 \neq p_2 \)),那么: +\[ +\exists d \in Z^+, +\begin{cases} +p_1 = n - d \\ +p_2 = n + d +\end{cases} +\]

+

那么这个 \( d \) 和 \( n \) 有什么关系呢?适逢当时被科普了 黎曼猜想, 得知黎曼 \( \Zeta \) 函数的非平凡零点很可能全部都位于直线 \( Re(z) = {1 \over 2} \) 上,那么会不会,最小的 \( d \) 也会在 \( n \cdot { 1 \over 2} \) 的边界内找得到呢?

+

于是提出了以下猜想命题: +\[ \tag{0} \label{0} +\forall n \in Z^+, n \gt M, \exists d \in Z^+, d \le { n \over 2 }, Prime(n - d) \land Prime(n + d) +\]

+

其中 \( M \) 为常数,表示命题成立在某个整数以上的整数范围成立。也就是,证明有穷个整数之后的整数都满足性质\( \ref{0} \),然后再逐个验证之前的整数满足该性质,就可以得出一个比偶数哥德巴赫猜想更充分的命题,从而直接可以推导出偶数哥德巴赫定理。

+

若质数p不能整除偶数2n,则p不整除2n-p

+

命题形式化描述

+

\[\label{1} \tag{1} +\forall p \in Primes, p \lt 2n(n \in Z^+), p \nmid 2n \Rightarrow p \nmid 2n - p +\]

+

一般化命题与反证法 1

+

我们可以抽出一个更一般的命题:对任意正整数 \(a, b (a \lt b)\),若 \( a \nmid b\),则 \( a \nmid b - a \): +\[\label{1.0} \tag{1.0} +\forall a, b \in Z^+ (a \lt b), a \nmid b \Rightarrow a \mid b - a +\]

+

反证法

+

假设原结论不成立,即 \( a \mid b - a \) 成立,则:

+

\( \because a \mid b - a, a \mid a \)

+

\( \therefore a \mid (b - a) + a \) (整除的线性性质),即 \( a \mid b \),

+

这与假设 \( a \nmid b \) 矛盾!故假设不成立,原结论成立,即 \( a\mid b - a \),

+

故命题 \( \ref{1.0} \) 成立。

+

代入条件

+

令 \( a := p, b = 2n \), 则显然:

+

\[ +p \in Primes, p \lt 2n \Rightarrow p \nmid 2n \Rightarrow p \nmid 2n - p +\]

+

即命题 \( \ref{1} \) 得证。

+
+

(以下内容为作者之前思考过的另外一个比较迂回的证明方法,为后续叙述的引理)

+

一整数若不同时整除互质两数,则不能整除后两数之差

+

形式化描述

+

\[ \label{1.1} \tag{1.1} +\forall x \in Z^+ \land x > 1, a \in Z^+ \land a > 1, b \in Z^+ \land b > 1; \quad +(a, b) = 1, x|a \lor x|b \Rightarrow x \nmid a-b +\]

+

证明

+

不妨设 \( a \gt b \)。易知\(x|a\)与\(x|b\)不同时成立, 否则\((a, b) \ge x\), 与 \((a, b) = 1\) 矛盾. +分类讨论:

+
    +
  • +

    \(x|a, , x \nmid b\)

    +

    \( \because x|a \)

    +

    \( \therefore \exists m \in Z^+, mx=a. \)

    +

    假设 \( x|a-b \), 即 \( \exists k \in Z^+, kx=a-b \Leftrightarrow b = a-kx = (m-k)x. \)

    +

    若 \( m = k \), 则 \( b = 0 \), 与 \( b > 1 \) 矛盾;

    +

    若 \( m \neq k \), 则 \( \exists (m-k) \in Z, b = (m-k)x \Leftrightarrow x|b \), 与假设 \( x \nmid b \) 矛盾!

    +

    故假设 \( x|a-b \) 不成立, \( x \nmid a-b. \)

    +
  • +
  • +

    \(x \nmid a, , x|b\). 同理可得: +若\( x|a-b\) , 则 \( a = b + kx = (m+k)x \Leftrightarrow x|a \), 与 \(x \nmid a\) 矛盾! 故 \(x \nmid a-b \).

    +
  • +
+

代入条件

+

令 \( x := p, a := 2n, b := p \),显然有\( p \mid p \)。故,当 \(p \nmid 2n \)时, \( p \nmid 2n - p \),即命题 \( \ref{1} \) 得证。

+
1 +

感谢中山大学数学专业的倪秉业师弟指出这个更简洁的证明方式!

+
+

整数和它两倍(一倍半)之间的合数,可以整除整数的阶乘

+

形式化描述

+

\[ \label{2.1} \tag{2.1} +\forall m \in Z^+, m \gt 8, n \in Z^+, m \lt n \lt 2m; \quad n \not \in Primes \Leftrightarrow n \mid m! +\]

+

证明

+

充分性

+

\[ \label{2.1.1} \tag{2.1.1} +n \not \in Primes \Rightarrow n \mid m! +\]

+

因为 \( n \) 为合数,将其分为完全平方数和不完全平方数两种情况证明。

+

不完全平方数

+

不妨设 \( n = ab (a \gt b \ge 2, (a, b) = 1) \),则 \(b \lt a \lt m \),证明如下:

+

若 \( a \ge m \),

+

则 \(n = ab \ge 2a \ge 2m \Leftrightarrow n \ge 2m \),

+

这与 \( n \lt 2m \) 矛盾!

+

故假设不成立,故 \(2 \le b \lt a \lt m \Rightarrow a \mid m!, b \mid m! \)。

+

又 \( (a, b) = 1 \Rightarrow [a, b] = ab = n \)

+

因此:\( a \mid m!, b \mid m \Rightarrow [a, b] \mid m! \Leftrightarrow n \mid m! \)。

+

完全平方数

+

不妨设 \( n = k^2 \),则 \(2 \lt k \lt 2k \lt m \),证明如下:

+

若 \( 2k \ge m \),则 \( k \ge { m \over 2 } \),

+

则 \(n = k^2 \ge { m^2 \over 4 } \ge { 8 / 4 } \cdot m = 2m \Leftrightarrow n \ge 2m \),

+

这与 \( n \lt 2m \) 矛盾!

+

故假设不成立,故 \( 2 \le k \lt 2k \lt m \Rightarrow 2k^2 \mid m! \)。

+

因此:\( 2k^2 \mid m! \Leftrightarrow 2n | m! \Rightarrow n \mid m! \)。

+

综上,命题 \( \ref{2.1.1} \) 得证。

+

必要性

+

\[ \label{2.1.2} \tag{2.1.2} +n \mid m! \Rightarrow n \not \in Primes +\]

+

反证:假设 \(n \in Primes \),则:

+

\( \because n > m, n \in Primes, \)

+

\( \therefore n \nmid 2, \space n \nmid 3, \space n \nmid 4, \space \dots, \space n \mid m \Rightarrow n \nmid m!, \)

+

这与条件 \( n \mid m! \) 矛盾!故假设不成立,命题 \( \ref{2.1.2} \) 得证。

+

综上,命题 \( \ref{2.1} \) 得证。

+

更强的结论

+

显然对 \( \forall n \in (m, 3/2m) \Rightarrow n \in (m, 2n) \),故: +\[ \label{2.2} \tag{2.2} +\forall m \in Z^+, m \gt 8, n \in Z^+, m \lt n \lt { 3m \over 2 }; \quad n \not \in Primes \Leftrightarrow n \mid m! +\]

+

用初等数论探索哥德巴赫猜想

+
+

在探索哥德巴赫猜想在初等数论框架内证明方式, 并由此发现一些显而易见的有趣结论: 若一个偶数2n能够拆分为两个奇素数的和的形式, 并且如果两个奇素数不相等, 那么这两个素数中较小的一个p(易知\( p \lt n \))必然不能整除n.

+
+

哥德巴赫猜想

+

关于历史研究历程一类资料, 请参考wiki.

+

本文将哥德巴赫猜想简单地描述为:

+
+

给定任意整数\(n(n > 1)\), 以及不超过n的所有素数1的集合\( P = \{ p \mid Prime(p) \land p \lt n \} \). 设\(p\)为集合\( P \)中的一个元素, 猜想\(p\)对应的整数\(2n-p\)所组成的集合\(2n-P\)中, 必然存在素数元素.

+
+

引理

+

(一) P中存在不整除n的元素

+

引理1: +\[ \label{1.1} \tag{1.1} +\exists p \in P, p \nmid n +\]

+

证明(反证法):

+

假设命题\( \ref{1.1} \) 的反命题: +\[\label{1.2} \tag{1.2} +\forall p \in P, p \mid n \Leftrightarrow \forall p \in P, \exists m \in Z, m \neq 0 \land mp = n +\] +成立, 由伯特兰-切比雪夫定理(Bertrand's postulate)可知: +\[ +\exists p_0, {n \over 2} \lt p_0 \lt n +\]

+

由假设\( \ref{1.2} \) 可得: +\[{mp_0 \over 2} \lt p_0 \Rightarrow m \lt 2\].

+

又\(m \in Z\), \(p_0 \gt 0, n \gt 0 \Rightarrow m = {n \over p_0} \gt 0\), 所以\(m = 1\), 即\(mp_0 = p_0 = n\), 这与\(p_0 \lt n\) 矛盾!

+

所以命题\( \ref{1.2} \)不成立. 故命题\( \ref{1.1} \)成立.

+

(二) 给定正整数 \(x\) 若整除互质数对\( a, b \)之一,则 \(x\) 不整除 \(a-b\)

+

\[ \label{2} \tag{2} +(a, b) = 1, x \gt 1, a \gt 1, b \gt 1, x|a \lor x|b \Rightarrow x \nmid a-b +\]

+

证明:

+

易知\(x|a\)与\(x|b\)不同时成立, 否则\((a, b) \ge x\), 与 \((a, b) = 1\) 矛盾. +分类讨论:

+
    +
  • +

    \(x|a, , x \nmid b\)

    +

    \( \because x|a \)

    +

    \( \therefore \exists m \ge 1, m \in Z, mx=a. \)

    +

    假设 \( x|a-b \), 即 \( \exists k \in Z, kx=a-b \Leftrightarrow b = a-kx = (m-k)x. \)

    +

    若 \( m = k \), 则 \( b = 0 \), 与 \( b > 1 \) 矛盾;

    +

    若 \( m \neq k \), 则 \( \exists (m-k) \in Z, b = (m-k)x \Leftrightarrow x|b \), 与假设 \( x \nmid b \) 矛盾!

    +

    故假设 \( x|a-b \) 不成立, \( x \nmid a-b. \)

    +
  • +
  • +

    \(x \nmid a, , x|b\). 同理可得: +若\( x|a-b\) , 则 \( a = b + kx = (m+k)x \Leftrightarrow x|a \), 与 \(x \nmid a\) 矛盾! 故 \(x \nmid a-b \).

    +
  • +
+

探索哥德巴赫猜想

+

分类讨论:

+
    +
  • +

    若n为素数, 显然 \( 2n-n = n \) 亦为素数, 哥德巴赫猜想成立. 1

    +
  • +
  • +

    若n为合数, 则 \( n \ge 4 \). 由引理(一), 将集合P以能否整除n划分为以下子集: \( S = \lbrace s \mid s|n \rbrace, T = \lbrace t \mid t \nmid n \rbrace\)

    +

    容易发现以下结论: \( \forall s \in S, \because s|n, s|s, \therefore s|2n-s \), 即若一个素数是n的素因子, 那么对应的整数 \( 2n-s \) 为合数. 故符合哥德巴赫猜想的数对 \( p \)与 \( 2n-p \)必然满足 \( p \nmid n \)(封面结论)

    +

    到这里, 我们可以得到一个哥德巴赫猜想的等价命题:

    +
    +

    对给定合数\(n\), 及小于\(n\)且不整除\(n\)的素数集合\( T = \lbrace t \mid Prime(t) \land t \lt n \land t \nmid n \rbrace \), 在集合\(T\)对应的整数集\(2n-T = \lbrace 2n - t \mid Prime(t) \land t \lt n \land t \nmid n \rbrace \)中是否存在素数

    +
    +
  • +
+

进一步探究

+

推论一

+

\[ \label{3} \tag{3} +\forall s \in S, t \in T, s \nmid 2n-t +\] +而由引理(二)可得:

+

对于任意前述s, t, 有:

+
    +
  1. \( Prime(s), Prime(t) \Rightarrow s \nmid t \)
  2. +
  3. \( s|n \Rightarrow s|2n \)
  4. +
+

故: \( \forall s \in S, t \in T, s \nmid 2n-t \) +也就是, 至少集合 \( 2n-T \) 的元素不会被 \( S \) 中的元素整除, 命题 \( \ref{3} \) 证毕。

+

推论二

+

对于\(T\)的子集 \( T_{\gt {n \over 2}} = T_1 = \lbrace t| t \in T, t \gt {n \over 2} \rbrace \), 具有以下性质:

+

\[ \label{4} \tag{4} +\forall t_1, t_2 \in T_1, t_1 \nmid 2n-t_2 +\]

+

证明如下:

+

假设 \( \exists t_1, t_2, t_1 \mid 2n-t_2 \), 即 \( \exists m \in Z^+, mt_1 = 2n - t_2 \).

+

\( \because t_1, t_2为奇数, 2n为偶数 \) +\( \therefore m为奇数. \)

+

分类讨论:

+
    +
  1. +

    当 \( m = 1 \) 时, t_1 + t_2 = 2n.

    +

    \( \because t_1 \neq t_2, t_1 \lt n, t_2 \lt n, \therefore t_1 + t_2 \lt 2n \), 矛盾!

    +
  2. +
  3. +

    当 \( m \ge 3 \)时:

    +

    \( +\because t_1 \gt {n \over 2}, t_2 \gt {n \over 2}, +\therefore mt_1 + t_2 \gt { mn \over 2 } + { n \over 2 } \ge { 3n \over 2 } + { n \over 2 } = 2n +\) +, 矛盾!

    +
  4. +
+

故假设不成立, 命题 \( \ref{4} \) 证毕.

+
1 +

如无特殊说明,本文中所有集合均为正整数集的子集

+
+
2 +

在本文中暂且抛去哥德巴赫猜想中对分解出的两个素数不能相等的要求。

+
+
+ +

黄俊杰

+

77u/KCs4NikxODgtMTk0OC0xMjYyDQo= · + huangjj.27@qq.com · + 349373001 · + huangjj27 · + huangjj27

+ +

下载简历pdf · + tech-blog: https://huangjj27.github.io · + 微信技术公众号: 坏姐姐日常入门Rust

+

教育背景

+

2013.9 -- 2017.7 中山大学 数据科学与计算机学院(原软件学院) 软件工程 工学学士

+

项目经历

+

数字化营业厅 2021.09 - 至今

+

测试工程师: 业务测试、自动化测试、性能测试

+
    +
  • 负责数字化营业厅项目下的叫号系统与数据赋能看板项目的功能测试
  • +
  • 针对热点性能的接口与流程进行性能测试
  • +
  • 辅助运营人员排查与定位营业员反馈的生产问题
  • +
+

Bonus:

+
    +
  • 编写接口自动化测试用例(Postman),并设定生产环境的每日自动巡检(企业微信 bot)
  • +
  • 使用 gooselocus框架编写性能测试用例
  • +
  • 制定性能测试需求评估、性能测试报告规范
  • +
  • review 项目代码,并通过项目代码补充对 DES、AES、HTTP设计规范的了解
  • +
+

银行业客户经理智能推荐与客户反馈收集项目 2020.11 - 2021.09

+

大数据开发工程师: 大数据 EDI 开发

+
    +
  • 负责子项目的架构优化、详细设计及部分代码实现
  • +
  • 使得原本执行需要 40 小时的作业降至平均完成时间 6 小时
  • +
  • 通过 GitLab 管理项目源代码,进行问题追踪、代码评审、自动化流水构建、自动化测试执行
  • +
  • 负责子项目程序优化,进行 excel 配置自动化转化为数据库工具的开发
  • +
  • 负责子项目部分功能的测试,利用了 Python 工具自动化执行测试用例
  • +
  • 负责子项目中关键词词频分析相关部分程序的维护
  • +
+

HiveQL 静态代码扫描检查工具 2019.4

+

大数据开发工程师: 大数据 EDI 开发

+
    +
  • 自发地将银行客户用于 Hive QL 静态扫描规则的工具 CLI 化改造(基于 Python)
  • +
  • 与上下游沟通,将该工具部署至 CI 平台
  • +
  • 该工具成功阻止多次高风险代码提交
  • +
  • 对相关员工讲演培训
  • +
+

客户个人金融业务管理平台 2018.8 -- 2019.4

+

大数据开发工程师: 大数据 EDI 开发

+
    +
  • 基于银行客户内 EDI 框架(基于 Hadoop 与 Hive)进项业务项目开发
  • +
  • 基于 Hive 特性与调度流程优化提高已有项目代码效率
  • +
  • 组织相关开发经验分享
  • +
+

(下列项目均为业余项目/开源贡献项目)

+

《Rust 中的异步编程》

+
    +
  • Asynchronous Programming in Rust 一书翻译
  • +
  • 该书详细地介绍了在 Rust 中异步编程的基础设施 Future trait、Waker类型,为了使 Future +正常工作的 Pin<T> 智能指针与 Unpin trait,以及方便开发而引用的 async/await 语法糖
  • +
  • 该书亦给出了示例构建一个简单的执行器,以及实现一个简单的利用异步优化性能的简单 HTTP 服务器
  • +
+

env_logger

+
    +
  • 在 std 环境下使用比较广泛的 logger
  • +
  • 为该库 实现了基础的 wasm32-unknown-unknown 目标的支持, 让该库支持浏览器环境
  • +
  • 因为内部结构实现的原因(formatter 格式化后记录丢失了 log::Level 信息,writter 直接 +使用前述记录写入日志),暂时未实现在浏览器环境中的 log 分级。
  • +
+

TLSSigAPI - 使用 Rust 重写 Tencent Login Service Signature API

+
    +
  • 参考了 Python 程序实现
  • +
  • 补足了单元测试用例、集成测试用例
  • +
+

技能

+
    +
  • 后端开发/web 开发 +
      +
    • 熟悉 Rust-lang,熟悉生命周期约束、所有权系统并对其进行分析
    • +
    • 熟悉面向对象编程的概念以及SOLID原则
    • +
    • 熟悉基本的算法与数据结构
    • +
    • 了解 RESTful API设计
    • +
    +
  • +
  • 版本管理 +
      +
    • 熟练使用 git/github/gitlab进行代码版本管理
    • +
    • 具有良好的版本管理意识, 熟悉 语义化版本 规则
    • +
    +
  • +
  • 软件测试 -- 有功能测试、单元测试、集成测试、接口测试经验,也熟悉使用 Rust 测试套件
  • +
  • 外语 -- 英语(CET6)
  • +
+ + +
+ +
+ + + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + diff --git a/reservoir.html b/reservoir.html new file mode 100644 index 0000000..748d308 --- /dev/null +++ b/reservoir.html @@ -0,0 +1,549 @@ + + + + + + 蓄水池算法改进 - 面向抽奖场景保证等概率性 - huangjj27's Tech blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+
+

蓄水池算法改进 - 面向抽奖场景保证等概率性

+
+

免责声明:禁止任何个人或团体使用本文研究成果用于实施任何违反中华人民共和国法律法规的活动 +如有违反,均与本文作者无关

+
+

在我们通常遇到的抽奖场景,于年会时将所有人的编号都放到箱子里面抽奖,然后每次抽出中奖者 +决定奖项。而在这过程中,因为先抽中者已经确定了奖项,然后不能够参与后续的奖项的抽奖;而后 +续参与抽奖的人员则其实会以越来越低的概率参与抽奖:

+
+

例:在上述场景中共有 \( n \) 人参与抽取 \( m ( \lt n) \) 个奖项,

+

抽取第一个奖项概率为: \( { m \over n } \)

+

那么如果第一个奖项被抽走并 揭露了,剩下 \( n - 1 \) 人参与 \( m - 1 \) 个奖项,抽中的概率 +为 \( m - 1 \over n - 1 \)。 +那么 \( m \lt n \Rightarrow -m \gt -n \Rightarrow mn - m \gt nm - n \Rightarrow m(n-1) \gt n(m - 1) \Rightarrow { m \over n } \gt { m - 1 \over n - 1 }\), +即如果是后续参与抽奖 并且前面的奖项被拿走了,后面抽到奖项的概率会更低,同时也会失去参与部分奖项的机会

+
+

因此,在人数 \( n \) 大于奖项数 \( m \) 的时候,我们通过以越来越低的概率干涉前面 +已经“取得”奖项的结果,来保证先参与抽奖的人中奖的概率随着人数的增多中奖的概率也变低, +最后保证每个人中奖的概率为 \( m \over n \)。但是在实际场景中,\( m \) 个奖项可能 +不仅相同(如划分了一二三等奖),因此对于蓄水池算法的改进提出了新的要求:

+
    +
  • 将所有的奖项视为各不相同的位置,不论人数多少(当还是要保证有人来参与抽奖 \( n \gt 1\) )所有人占有特定位置的概率相同
  • +
  • 每当新来一人参与抽奖时,如果他没有中奖,可以即场告知未中1
  • +
+

算法描述与等概率性证明

+

我们分两种情况讨论:

+
    +
  • 一种是当人数不足以覆盖所有的奖项的场景( \(n \lt m \) ),
  • +
  • 另外一种是当抽奖人数远大于所有奖项加起来的数目。( \( n \gt m \))。
  • +
+

然后我们再回来看看能不能找到一种很方便的方法桥接两种情况。

+

同时,我们假设 \( m \) 个奖项两两互不相同。

+

抽奖人数不足时( \(n \lt m \) )

+

因为当人数不足时,所有参与者都能抽奖,因此我们要保证每个人获得特定奖项的概率为 \( 1 \over m \)。 +算法描述:

+
+

记 \( Choosen \) 为容量为 \( m \) 的数组, +\( Choosen[k] (1 \le k \le m) \) 表示第 k 个奖项的当前占有情况, +初始值为 \( None \),

+

\( Players \) 为参与参与抽奖的人的序列

+
    +
  1. 令 \( i := 1 \),当 \( i \le n \) 时,做如下操作: +
      +
    • 产生随机数 \( r_1 (1 \le r_1 \le i) \)
    • +
    • 如果 \( r_1 \lt i \),\( Choosen[i] := Choosen[r_1] \)
    • +
    • \( Choosen[r_1] := Players[i] \)
    • +
    • \( i := i + 1 \)
    • +
    +
  2. +
  3. 当 \( i \le m \) 时,做如下操作: +
      +
    • 产生随机数 \( r_2 (1 \le r_2 \le i) \)
    • +
    • 如果 \( r_2 \lt i \): +
        +
      • \( Choosen[i] := Choosen[r_2] \)
      • +
      • \( Choosen[r_2] := None \)
      • +
      +
    • +
    • \( i := i + 1 \)
    • +
    +
  4. +
+
+

等概率性证明

+

我们先证明,在填入中奖者的第 \( k (1 \le k \le m) \) 轮过程中,能够保证对于前 \( k \) +个奖项中的每一个奖项,每一位中奖者抽中其中第 \( i (1 \le i \le k) \) 个奖项的概率为 +\(1 \over k \),证明如下:

+

我们采用数学归纳法来证明:

+
    +
  1. 奠基:当 \( k = 1 \) 时,易知该中奖者一定会抽中第一个奖项,前一个奖项中只有第一个 +选项,所以此时每一位中奖者抽中第 \( k = 1 \) 的概率为 \( 1 = { 1 \over 1 } = { 1 \over k } \);
  2. +
  3. 归纳: +
      +
    • 假设当 \(k = j (1 \le j \lt m) \)时,每一位抽奖者抽中第 \( i (1 \le i \le j) \)的概率为 +\( 1 \over j \)
    • +
    • 当 \( k = j + 1 \), 有: +
        +
      • 第 \( j + 1 \) 位抽奖着抽中任意第 \( i' (1 \le i' \le j + 1) \) 个奖项的概率为 \( 1 \over { j + 1 } \) +(假设产生的随机数 \( r_1、r_2 \) 足够的均匀);
      • +
      • 对于前 \( j \) 位抽奖者,每一位都有 \( 1 \over { j + 1 } \),的概率将自己的奖项更换位第 \( j + 1 \)个奖项;
      • +
      • 对于前 \( j \) 位抽奖者,每一位依然占有原有第 \( i' \) 个奖项的概率为:
      • +
      +
    • +
    +
  4. +
+

\[ \begin{equation} +\begin{aligned} +P\{前 j 位抽奖者 j + 1 轮中仍然持有 i' \} & = P\{前 j 位抽奖者j轮已经持有 i' \} \cdot P\{第 j + 1 位抽奖者没有抽中 i' \} \\ +& = P\{前 j 位抽奖者j轮已经持有 i' \} \cdot (1 - P\{第 j + 1 位抽奖者抽中 i' \}) \\ +& = \frac{1}{j} \cdot (1 - \frac{1}{j+1}) \\ +& = \frac{1}{j} \cdot \frac{j}{j+1} \\ +& = \frac{1}{j + 1} \\ +& = \frac{1}{k} \\ +\end{aligned} +\label{1.1} \tag{1.1} +\end{equation} +\]

+

由上,可知每一轮迭代之后,前 \( k \) 个奖项对于已经参与的 \( k \)中奖者来说抽中的概率均等,为 \( 1 \over k \), +故到了第 \( n \) 轮操作后,我们可以通过不断填充 \( None \)值来稀释概率,最后达到 \( 1 \over m \) 的等概率性。

+

特殊地,当 \( n == m \) 时,每个抽奖者抽到特定奖项的概率也为 \(1 \over n \)。

+

抽奖人数足够多时( \(n \gt m \) )

+

类似地,当 \(n \gt m \)时,对于每一个抽奖序号 \( k \gt m \) 的抽奖者,我们生成随机数 \( r_3(1 \le r_3 \le n) \),并且在 +\( r_3 \le m \) 的时候,替换对应原本占有奖项的抽奖者;可以证明在这种情况下,能保证每个人抽到特定奖项的概率为 \(1 \over n \)2

+

整合后的算法

+
+

记 \( Choosen \) 为容量为 \( m \) 的数组, +\( Choosen[k] (1 \le k \le m) \) 表示第 \( k \) 个奖项的当前占有情况, +初始值为 \( None \),

+

\( replaced \) 为原本已经中奖,但是被人替换的抽奖者

+

\( Players \) 为参与参与抽奖的人的序列,每次只能获取一个 \( player \)

+

记 \( n := 0 \)为当前参与抽奖的人数

+
    +
  1. 在抽奖结束前,每次遇到一个新的 \( player \) 执行以下操作: +
      +
    • \( replaced := None \)
    • +
    • \( n := n + 1 \)
    • +
    • 产生随机数 \( r (1 \le r \le n) \)
    • +
    • 如果 \( r \le m \): +
        +
      • \( replaced := Choosen[r] \)
      • +
      • \( Choosen[r] := player \)
      • +
      +
    • +
    • 如果 \( r \lt n \) 并且 \( n \le m \): +
        +
      • \( Choosen[n] := replaced \)
      • +
      +
    • +
    +
  2. +
  3. 在抽奖结束时,如果 \( n \lt m \), 执行以下操作: +
      +
    • \( i := n \)
    • +
    • 当 \( i \lt m \)时,重复执行以下操作: +
        +
      • \( i := i + 1 \)
      • +
      • 产生随机数 \( r_2 (1 \le r_2 \le i) \)
      • +
      • 如果 \( r_2 \lt i \): +
          +
        • \( Choosen[i] := Choosen[r_2] \)
        • +
        • \( Choosen[r_2] := None \)
        • +
        +
      • +
      +
    • +
    +
  4. +
+
+

程序实现

+

Rust

+

作者偏好 Rust 编程语言,故使用 Rust 实现。

+

特质(trait)

+

Rust 中的特质(trait) +是其用于复用行为抽象的特性,尽管比起 Java 或 C# 的接口 (Interface)更加强大,但在此文中, +熟悉 Java/C# 的读者把特质视作接口就可以了。

+

建模与实现

+

本文使用面向对象(Object-Oriented)编程范式3来进行抽象,如下所示:

+
use rand::random;
+
+use std::fmt::Debug;
+
+trait ReservoirSampler {
+    // 每种抽样器只会在一种总体中抽样,而总体中所有个体都属于相同类型
+    type Item;
+
+    // 流式采样器无法知道总体数据有多少个样本,因此只逐个处理,并返回是否将样本纳入
+    // 样本池的结果,以及可能被替换出来的样本
+    fn sample(&mut self, it: Self::Item) -> (bool, Option<Self::Item>);
+
+    // 任意时候应当知道当前蓄水池的状态
+    fn samples(&self) -> &[Option<Self::Item>];
+}
+
+struct Lottery<P> {
+    // 记录当前参与的总人数
+    total: usize,
+
+    // 奖品的名称与人数
+    prices: Vec<Price>,
+
+    // 当前的幸运儿
+    lucky: Vec<Option<P>>,
+}
+
+#[derive(Clone, Debug)]
+struct Price {
+    name: String,
+    cap: usize,
+}
+
+impl<P> ReservoirSampler for Lottery<P> {
+    type Item = P;
+
+    fn sample(&mut self, it: Self::Item) -> (bool, Option<Self::Item>) {
+        let lucky_cap = self.lucky.capacity();
+
+        self.total += 1;
+
+        // 概率渐小的随机替换
+        let r = random::<usize>() % self.total + 1;
+        let mut replaced = None;
+        if r <= lucky_cap {
+            replaced = self.lucky[r - 1].take();
+            self.lucky[r - 1] = Some(it);
+        }
+
+        if self.total <= lucky_cap && r < self.total {
+            self.lucky[self.total - 1] = replaced.take();
+        }
+
+        (r <= lucky_cap, replaced)
+    }
+
+    fn samples(&self) -> &[Option<Self::Item>] {
+        &self.lucky[..]
+    }
+}
+
+impl<P: Debug> Lottery<P> {
+    fn release(self) -> Result<Vec<(String, Vec<P>)>, &'static str> {
+        let lucky_cap = self.lucky.capacity();
+
+        if self.lucky.len() == 0 {
+            return Err("No one attended to the lottery!");
+        }
+
+        let mut final_lucky = self.lucky.into_iter().collect::<Vec<Option<P>>>();
+        let mut i = self.total;
+        while i < lucky_cap {
+            i += 1;
+
+            // 概率渐小的随机替换
+            let r = random::<usize>() % i + 1;
+            if r <= lucky_cap {
+                final_lucky[i - 1] = final_lucky[r - 1].take();
+            }
+        }
+        println!("{:?}", final_lucky);
+
+        let mut result = Vec::with_capacity(self.prices.len());
+        let mut counted = 0;
+        for p in self.prices {
+            let mut luck = Vec::with_capacity(p.cap);
+
+            for i in 0 .. p.cap {
+                if let Some(it) = final_lucky[counted + i].take() {
+                    luck.push(it);
+                }
+            }
+
+            result.push((p.name, luck));
+            counted += p.cap;
+        }
+
+        Ok(result)
+    }
+}
+
+// 构建者模式(Builder Pattern),将所有可能的初始化行为提取到单独的构建者结构中,以保证初始化
+// 后的对象(Target)的数据可靠性。此处用以保证所有奖品都确定后才能开始抽奖
+struct LotteryBuilder {
+    prices: Vec<Price>,
+}
+
+impl LotteryBuilder {
+    fn new() -> Self {
+        LotteryBuilder {
+            prices: Vec::new(),
+        }
+    }
+
+    fn add_price(&mut self, name: &str, cap: usize) -> &mut Self {
+        self.prices.push(Price { name: name.into(), cap });
+        self
+    }
+
+    fn build<P: Clone>(&self) -> Lottery<P> {
+        let lucky_cap = self.prices.iter()
+            .map(|p| p.cap)
+            .sum::<usize>();
+
+        Lottery {
+            total: 0,
+            prices: self.prices.clone(),
+            lucky: std::vec::from_elem(Option::<P>::None, lucky_cap),
+        }
+    }
+}
+
+fn main() {
+    let v = vec![8, 1, 1, 9, 2];
+    let mut lottery = LotteryBuilder::new()
+        .add_price("一等奖", 1)
+        .add_price("二等奖", 1)
+        .add_price("三等奖", 5)
+        .build::<usize>();
+
+
+    for it in v {
+        lottery.sample(it);
+        println!("{:?}", lottery.samples());
+    }
+
+    println!("{:?}", lottery.release().unwrap());
+}
+
+

优点

+
    +
  • 流式处理,可以适应任意规模的参与人群
  • +
  • 在保证每一位抽奖者都有相同的概率获得特定奖项的同时,还能保证每一个抽奖者的获得的奖项均不相同
  • +
+

缺点

+
    +
  • 所有参与抽奖的人都必须依次经过服务器处理,因为需要获知准确的总人数来保证等概率性。 +一个改进的方法是,在人数足够多的时候,将总人数用总人数的特定数量级替代(给后续参加者的 +一点点小福利——但是因为总人数足够多,所以总体中奖概率还是很低),在客户端完成中奖的选定
  • +
  • 等概率性完全依赖随机数 r 生成。 因为奖品初始化时不需要考虑打乱顺序,因此如果在 +随机这一步被技术破解,使得抽奖者可以选择自己能获取的奖项,则会破坏公平性。改进方案是, +在 release 的时候再一次对奖品顺序进行随机的打乱。
  • +
  • 这种抽奖方式还限定了每人只能抽取一次奖品,否则会出现一个人占有多个奖项的情况。
  • +
+
2 +

可以参考博主以前的博客

+
+
3 +

作者理解的面向对象 = 对象是交互的最基本单元 + 对象通过相互发送消息进行交互。而特质/接口以及对象其他公开的方法定义了对象可以向外发送/从外接收的消息。

+
+
1 +

该条件为用以减轻开奖时发通知的压力,并非核心需求,因为对参与抽奖的玩家负责的原因,我们还是需要储存每个玩家每次的抽奖情况信息

+
+

下一步可能展开的工作

+

目前所有抽奖者都按照相等的概率抽奖,而在一些场景下可能按照一些规则给与某些抽奖者优惠 +(例如绩效越高的员工中奖概率越大),因此下一步可能考虑如何按照权重赋予每位抽奖者各自的 +中奖概率。

+

致谢

+

感谢茶壶君(@ksqsf)一语惊醒梦中人,清楚明确地表达了需求; +感谢张汉东老师 (@ZhangHanDong)老师提点了之后可以开展研究的方向; +感谢在这次讨论中提供意见的其他 Rust 社区的朋友,谢谢你们!

+ +
+ +
+ + + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + diff --git a/resume.html b/resume.html new file mode 100644 index 0000000..3870a65 --- /dev/null +++ b/resume.html @@ -0,0 +1,310 @@ + + + + + + 关于我 - huangjj27's Tech blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+
+ + +

黄俊杰

+

77u/KCs4NikxODgtMTk0OC0xMjYyDQo= · + huangjj.27@qq.com · + 349373001 · + huangjj27 · + huangjj27

+ +

下载简历pdf · + tech-blog: https://huangjj27.github.io · + 微信技术公众号: 坏姐姐日常入门Rust

+

教育背景

+

2013.9 -- 2017.7 中山大学 数据科学与计算机学院(原软件学院) 软件工程 工学学士

+

项目经历

+

数字化营业厅 2021.09 - 至今

+

测试工程师: 业务测试、自动化测试、性能测试

+
    +
  • 负责数字化营业厅项目下的叫号系统与数据赋能看板项目的功能测试
  • +
  • 针对热点性能的接口与流程进行性能测试
  • +
  • 辅助运营人员排查与定位营业员反馈的生产问题
  • +
+

Bonus:

+
    +
  • 编写接口自动化测试用例(Postman),并设定生产环境的每日自动巡检(企业微信 bot)
  • +
  • 使用 gooselocus框架编写性能测试用例
  • +
  • 制定性能测试需求评估、性能测试报告规范
  • +
  • review 项目代码,并通过项目代码补充对 DES、AES、HTTP设计规范的了解
  • +
+

银行业客户经理智能推荐与客户反馈收集项目 2020.11 - 2021.09

+

大数据开发工程师: 大数据 EDI 开发

+
    +
  • 负责子项目的架构优化、详细设计及部分代码实现
  • +
  • 使得原本执行需要 40 小时的作业降至平均完成时间 6 小时
  • +
  • 通过 GitLab 管理项目源代码,进行问题追踪、代码评审、自动化流水构建、自动化测试执行
  • +
  • 负责子项目程序优化,进行 excel 配置自动化转化为数据库工具的开发
  • +
  • 负责子项目部分功能的测试,利用了 Python 工具自动化执行测试用例
  • +
  • 负责子项目中关键词词频分析相关部分程序的维护
  • +
+

HiveQL 静态代码扫描检查工具 2019.4

+

大数据开发工程师: 大数据 EDI 开发

+
    +
  • 自发地将银行客户用于 Hive QL 静态扫描规则的工具 CLI 化改造(基于 Python)
  • +
  • 与上下游沟通,将该工具部署至 CI 平台
  • +
  • 该工具成功阻止多次高风险代码提交
  • +
  • 对相关员工讲演培训
  • +
+

客户个人金融业务管理平台 2018.8 -- 2019.4

+

大数据开发工程师: 大数据 EDI 开发

+
    +
  • 基于银行客户内 EDI 框架(基于 Hadoop 与 Hive)进项业务项目开发
  • +
  • 基于 Hive 特性与调度流程优化提高已有项目代码效率
  • +
  • 组织相关开发经验分享
  • +
+

(下列项目均为业余项目/开源贡献项目)

+

《Rust 中的异步编程》

+
    +
  • Asynchronous Programming in Rust 一书翻译
  • +
  • 该书详细地介绍了在 Rust 中异步编程的基础设施 Future trait、Waker类型,为了使 Future +正常工作的 Pin<T> 智能指针与 Unpin trait,以及方便开发而引用的 async/await 语法糖
  • +
  • 该书亦给出了示例构建一个简单的执行器,以及实现一个简单的利用异步优化性能的简单 HTTP 服务器
  • +
+

env_logger

+
    +
  • 在 std 环境下使用比较广泛的 logger
  • +
  • 为该库 实现了基础的 wasm32-unknown-unknown 目标的支持, 让该库支持浏览器环境
  • +
  • 因为内部结构实现的原因(formatter 格式化后记录丢失了 log::Level 信息,writter 直接 +使用前述记录写入日志),暂时未实现在浏览器环境中的 log 分级。
  • +
+

TLSSigAPI - 使用 Rust 重写 Tencent Login Service Signature API

+
    +
  • 参考了 Python 程序实现
  • +
  • 补足了单元测试用例、集成测试用例
  • +
+

技能

+
    +
  • 后端开发/web 开发 +
      +
    • 熟悉 Rust-lang,熟悉生命周期约束、所有权系统并对其进行分析
    • +
    • 熟悉面向对象编程的概念以及SOLID原则
    • +
    • 熟悉基本的算法与数据结构
    • +
    • 了解 RESTful API设计
    • +
    +
  • +
  • 版本管理 +
      +
    • 熟练使用 git/github/gitlab进行代码版本管理
    • +
    • 具有良好的版本管理意识, 熟悉 语义化版本 规则
    • +
    +
  • +
  • 软件测试 -- 有功能测试、单元测试、集成测试、接口测试经验,也熟悉使用 Rust 测试套件
  • +
  • 外语 -- 英语(CET6)
  • +
+ + +
+ +
+ + + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + diff --git "a/resume/\351\273\204\344\277\212\346\235\260-Rust.pdf" "b/resume/\351\273\204\344\277\212\346\235\260-Rust.pdf" new file mode 100644 index 0000000..1365b85 Binary files /dev/null and "b/resume/\351\273\204\344\277\212\346\235\260-Rust.pdf" differ diff --git a/rust-ffi.html b/rust-ffi.html new file mode 100644 index 0000000..6837e03 --- /dev/null +++ b/rust-ffi.html @@ -0,0 +1,438 @@ + + + + + + 在 WSL 中学习 Rust FFI - huangjj27's Tech blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+
+

在 WSL 中学习 Rust FFI

+
+

博主最近从新学习 Rust FFI 的使用,但是手头上没有可用的 Linux 环境(Windows 编译c太麻烦了),于是就尝试着使用 WSL来搭建 Rust 环境和简易的 c 编译环境,并记录下中间遇到的一些坑。感谢 Unsafe Rust 群群友 @框框 对本文的首发赞助!感谢 Rust 深水群 @栗子 的 gcc 指导!

+
+

阅读须知

+

阅读本文,你可以知道:

+
    +
  • 一些配置 WSL 全局变量的技巧
  • +
  • 快速配置 Rust 编译运行环境
  • +
  • 简单的 gcc 编译技巧
  • +
+

但是,本文不涉及:

+ +

WSL Rust 环境搭建

+

由于 WSL 是新装的,没有 Rust 和 gcc/g++ 环境,因此需要安装:

+
sudo apt install gcc -y
+
+# 官方脚本
+curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
+
+ +

但是由于在国内访问 Rust 官方网站会很慢,因此设置镜像到 Windows 环境变量中:

+
RUSTUP_DIST_SERVER=https://mirrors.ustc.edu.cn/rust-static
+RUSTUP_UPDATE_ROOT=https://mirrors.ustc.edu.cn/rust-static/rustup
+
+

然后,使用 WSLENV环境变量将上述变量共享到 WSL 中:

+
WSLENV=RUSTUP_DIST_SERVER:RUSTUP_UPDATE_ROOT
+
+

然后重启 WSL 终端,重新执行 Rust 一键脚本。

+

以下两个项目均来自 《Rust编程之道》一书,源代码仓库在这里

+

Rust 调用 C/C++

+

Rust 调用 C/C++ 代码可以使用 cc crate 配合 build.rs 预先编译好 C/C++ 的程序提供给 Rust 调用。

+

首先,创建一个 binary 项目:

+
cargo new --bin ffi_learn
+
+

项目目录结构如下:

+
cpp_src
+    |-- sorting.h
+    |-- sorting.cpp
+src
+    |-- main.rs
+Cargo.toml
+build.rs
+
+

然后编写 sorting.hsorting.cpp:

+
// sorting.h
+#ifndef __SORTING_H__
+#define __SORTING_H__ "sorting.h"
+#include <iostream>
+#include <functional>
+#include <algorithm>
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+void interop_sort(int[], size_t);
+
+#ifdef __cplusplus
+}
+#endif
+#endif
+
+
// sorting.cpp
+#include "sorting.h"
+
+void interop_sort(int numbers[], size_t size) {
+    int* start = &numbers[0];
+    int* end = &numbers[0] + size;
+
+    std::sort(start, end, [](int x, int y) { return x > y; });
+}
+
+

然后给 Cargo.toml[build-dependecies] 加上 cc crate 依赖:

+
# Cargo.toml
+# 其他配置
+
+[build-dependencies]
+cc = "1"
+
+

接着,我们通过 cc 调用对应平台的c/c++编译器,因为我们这个项目是 WSL,所以和调用我们刚安装的 gcc:

+
// build.rs
+// Rust 2018 不需要 extern crate 语句
+
+fn main() {
+    cc::Build::new()
+        .cpp(true)
+        .warnings(true)
+        .flag("-Wall")
+        .flag("-std=c++14")
+        .flag("-c")
+        .file("cpp_src/sorting.cpp")
+        .compile("sorting");    // sorting.so
+}
+
+

接着,我们在 Rust 主程序中,通过 extern 块引入sorting.cpp中的interop_sort函数,并调用它:

+
// main.rs
+#[link(name = "sorting", kind = "static")]
+extern "C" {
+    fn interop_sort(arr: *mut i32, n: u32);
+}
+
+pub fn sort_from_cpp(arr: &mut [i32]) {
+    unsafe {
+        // 传入必然有效的数组引用,并通过传入数组的长度来保证不会出现越界访问,从而保证函数内存安全
+        interop_sort(arr as *mut [i32] as *mut i32, arr.len() as u32);
+    }
+}
+
+fn main() {
+    let mut my_arr: [i32; 10] = [10, 42, -9, 12, 8, 25, 7, 13, 55, -1];
+    println!("Before sorting...");
+    println!("{:?}\n", my_arr);
+
+    sort_from_cpp(&mut my_arr);
+
+    println!("After sorting...");
+    println!("{:?}\n", my_arr);
+}
+
+

然后执行调用:

+
$ cargo run
+   Compiling ffi_learning v0.1.0 (/mnt/c/Users/huangjj27/Documents/codes/ffi_learning)
+warning: `extern` block uses type `[i32]`, which is not FFI-safe
+ --> src/main.rs:3:26
+  |
+3 |     fn interop_sort(arr: &[i32], n: u32);
+  |                          ^^^^^^ not FFI-safe
+  |
+  = note: `#[warn(improper_ctypes)]` on by default
+  = help: consider using a raw pointer instead
+  = note: slices have no C equivalent
+
+    Finished dev [unoptimized + debuginfo] target(s) in 4.71s
+     Running `target/debug/ffi_learn`
+Before sorting...
+[10, 42, -9, 12, 8, 25, 7, 13, 55, -1]
+
+After sorting...
+[55, 42, 25, 13, 12, 10, 8, 7, -1, -9]
+
+

我们看到,该函数提示我们 C 中并没有等价于 Rust slice 的类型,原因在于如果我们传递 slice,那么在 C/C++ 中就很容易访问超过数组长度的内存,造成内存不安全问题。但是,我们在 Rust 调用的时候,通过同时传入数组 arr 的长度 arr.len(), 来保证函数不会访问未经授权的内存。不过在实践中,应该划分模块,只允许确认过 内存安全的 safe Rust 功能跨越模块调用。

+

在 C/C++ 中调用 Rust

+

接下来我们反过来互操作。项目结构如下:

+
c_src
+    |-- main.c
+src
+    |-- lib.rs
+    |-- callrust.h
+Cargo.toml
+makefile
+
+

然后配置 Rust 生成两种库——静态库(staticlib)和c动态库(cdylib):

+
# Cargo.toml
+# ...
+
+[lib]
+name = "callrust"   # 链接库名字
+crate-type = ["staticlib", "cdylib"]
+
+

然后添加我们的 Rust 函数:

+
#![allow(unused)]
+fn main() {
+// lib.rs
+
+// `#[no_mangle]` 关闭混淆功能以让 C 程序找到调用的函数
+// `extern` 默认导出为 C ABI
+#[no_mangle]
+pub extern fn print_hello_from_rust() {
+    println!("Hello from rust");
+}
+}
+
+

当然,为了给 C 调用我们还需要编写一个头文件:

+
// callrust.h
+void print_hello_from_rust();
+
+

在我们的 main.c 中库并调用:

+
// main.c
+#include "callrust.h"
+#include <stdio.h>
+#include <stdint.h>
+#include <inttypes.h>
+
+int main(void) {
+    print_hello_from_rust();
+}
+
+ +

编写 makefile,先调度cargo 编译出我们需要的 Rust 库(动态或链接),然后再运行:

+
GCC_BIN ?= $(shell which gcc)
+CARGO_BIN ?= $(shell which cargo)
+
+# 动态链接 libcallrust.so
+share: clean cargo
+	mkdir cbin
+	$(GCC_BIN) -o ./cbin/main ./c_src/main.c -I./src -L./target/debug -lcallrust
+
+	# 注意动态链接再运行时也需要再次指定 `.so` 文件所在目录,否则会报错找不到!
+	LD_LIBRARY_PATH=./target/debug ./cbin/main
+
+# 静态链接 libcallrust.a
+static: clean cargo
+	mkdir cbin
+
+	# libcallrust.a 缺少了一些pthread, dl类函数,需要链接进来
+	$(GCC_BIN) -o ./cbin/main ./c_src/main.c -I./src ./target/debug/libcallrust.a -lpthread -ldl
+	./cbin/main
+
+clean:
+	$(CARGO_BIN) clean
+	rm -rf ./cbin
+
+cargo:
+	$(CARGO_BIN) build
+
+

小结

+

本文通过给出两个简单的示例来展示 Rust 通过 FFI 功能与 C/C++ 生态进行交互的能力, 并且指出几个在实践过程中容易浪费时间的坑:

+
    +
  1. WSL的环境变量不生效 -> 使用 WSLENV 变量从 Windows 引入使用。
  2. +
  3. make share 的时候提示 libcallrust.so 找不到 -> 需要在运行时指定 LD_LIBRARY_PATH 变量,引入我们编译的 libcallrust.so 路径。
  4. +
  5. make static的时候遇到了pthread_* dy*系列函数未定义问题 -> 通过动态链接系统库来支持运行。
  6. +
+ +
+ +
+ + + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + diff --git a/rust-mirror.html b/rust-mirror.html new file mode 100644 index 0000000..90e62f2 --- /dev/null +++ b/rust-mirror.html @@ -0,0 +1,277 @@ + + + + + + Rust 镜像 - huangjj27's Tech blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+
+

Rust 镜像

+
+

由于在 Rust 群经常有新人重复提问诸如 “Rust 下载很慢,怎么办?” “Rust 怎么安装更快” 一类的提问,因此整理国内常用的用于加速的镜像和反向代理。

+
+

阅读须知

+

本文将不涉及:

+ +

使用国内镜像加速更新 Rustup 工具链

+

我们需要指定 RUSTUP_DIST_SERVER(默认指向 https://static.rust-lang.org)和 RUSTUP_UPDATE_ROOT (默认指向https://static.rust-lang.org/rustup),这两个网站均在中国大陆境外,因此在中国大陆访问会很慢,需要配置成境内的镜像。

+

以下 RUSTUP_DIST_SERVERRUSTUP_UPDATE_ROOT 可以组合使用。

+
# 清华大学
+RUSTUP_DIST_SERVER=https://mirrors.tuna.tsinghua.edu.cn/rustup
+
+# 中国科学技术大学
+RUSTUP_DIST_SERVER=https://mirrors.ustc.edu.cn/rust-static
+RUSTUP_UPDATE_ROOT=https://mirrors.ustc.edu.cn/rust-static/rustup
+
+# 上海交通大学
+RUSTUP_DIST_SERVER=https://mirrors.sjtug.sjtu.edu.cn/rust-static/
+
+

+

使用国内镜像加速更新 crate 拉取

+

将如下配置写入 $HOME/.cargo/config 文件1 2

+
# 放到 `$HOME/.cargo/config` 文件中
+[source.crates-io]
+registry = "https://github.com/rust-lang/crates.io-index"
+
+# 替换成你偏好的镜像源
+replace-with = 'sjtu'
+
+# 清华大学
+[source.tuna]
+registry = "https://mirrors.tuna.tsinghua.edu.cn/git/crates.io-index.git"
+
+# 中国科学技术大学
+[source.ustc]
+registry = "git://mirrors.ustc.edu.cn/crates.io-index"
+
+# 上海交通大学
+[source.sjtu]
+registry = "https://mirrors.sjtug.sjtu.edu.cn/git/crates.io-index"
+
+# rustcc社区 - 已失效!
+# [source.rustcc]
+# registry = "https://code.aliyun.com/rustcc/crates.io-index.git"
+
+# rustcc 1号源
+[source.rustcc]
+registry="git://crates.rustcc.com/crates.io-index"
+
+# rustcc 2号源
+[source.rustcc2]
+registry="git://crates.rustcc.cn/crates.io-index"
+
+

参考资料:

+

Rustup 镜像安装帮助 - 清华大学

+

Rust Toolchain 反向代理使用帮助 - 中国科学技术大学

+

国内Rust库文件镜像 - rustcc

+

在中国大陆cargo命令速度很慢怎么办?

+
1 +

如遇到 invalid UTF-8 的问题请去掉文中中文注释

+
+
2 +

建议不要用Windows默认的记事本编辑

+
+ +
+ +
+ + + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + diff --git a/rust-patterns/abstract-factory.html b/rust-patterns/abstract-factory.html new file mode 100644 index 0000000..e4a17f0 --- /dev/null +++ b/rust-patterns/abstract-factory.html @@ -0,0 +1,324 @@ + + + + + + 抽象工厂(Abstract Factory)模式 - huangjj27's Tech blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+
+

抽象工厂(Abstract Factory)模式

+

场景需求

+

我们在设计游戏的时候,经常会遇到设计类似关卡的需求,例如 RPG 游戏中的副本(dungeon),每种副本都循序类似的模式、类似的行为,但是采用资源不同,行为的细节可能有所差异,为了方便我们新增新的副本,同时使得原有副本的修改尽可能少影响游戏的运行机制(所谓符合开闭原则),我们可以采用抽象工厂模式来统一管理游戏副本的生成。

+

场景设计

+

我们先设计一个简单的副本模式,包含了一个 Boss 和若干小怪,小怪数量在范围内随机波动。小怪和 Boss 都拥有血量,但是只有Boss 能攻击。我们把这个模式定义为结构:

+
struct Dungeon {
+    boss: Box<dyn Boss>,
+    monsters: Vec<Box<dyn Monster>>,
+}
+
+trait Monster {
+    // 获知当前血量
+    fn life(&self) -> u64;
+
+    // 怪物血量可以因受到攻击降低,也可自行回复
+    fn change_life(&mut self, diff: i32);
+}
+
+trait Boss: Monster {
+    // Boss 能攻击玩家(Hero)
+    fn attack(&self target: &mut Box<dyn Hero>);
+}
+
+

上面在运行时已经不关心具体是什么副本(当然,副本的元数据可以添加为 Dungeon 结构的字段),我们只需要知道它有一个 Boss 和若干 Monster

+

然后,我们建立抽象工厂:

+
use rand::random;
+trait DungeonFactory {
+    fn create_boss(&self) -> Box<dyn Boss>
+    fn create_mosters(&self, amount: usize) -> Vec<Box<dyn Monster>>
+
+    fn generate_dungeon(&self) -> Dungeon {
+        Dungeon {
+            boss: self.create_boss(),
+            monsters: self.create_monsters(random::<u8>() % 2 + 2), // 2 ~ 3 只小怪
+        }
+    }
+}
+
+

然后,我们创建新手副本的具体工厂:

+
struct NewbieDungeonFactory;
+
+impl DungeonFactory for NewBieDungeonFactory {
+    fn create_boss(&self) -> Box<dyn Boss> {
+        NewBieBoss::new()
+    }
+
+    // 新手副本甚至没有小怪,
+    fn create_mosters(&self, amount: usize) -> Vec<Box<dyn Monster>> {
+        Vec::new()
+    }
+
+    // generate_dungeon 已有默认实现,不需要修改。
+}
+
+// 新手副本,boss极弱
+struct NewbieBoss {
+    max_life: u8,
+    life: u8,
+    attack: u8,
+}
+
+impl NewbieBoss {
+    fn new() -> Self {
+        NewBieBoss {
+            max_life: 5,
+            life: 5,
+            attack: 1,
+        }
+    }
+}
+
+impl Monster for NewBieBoss {
+    fn life(&self) -> u64 {
+        self.life as u64
+    }
+
+    fn change_life(&mut self, diff: i32) {
+        self.life = match self.life + diff {
+            n if n < 0 => 0,
+            n if n > self.max_life => self.max_life
+            n => n
+        };
+    }
+}
+
+impl Boss for NewbieBoss {
+    fn attack(&self, target: &mut Box<dyn Hero>) {
+        target.attacked_wtih(self.attack);
+    }
+}
+
+

上面的工厂看上去好像很多内容,主要是因为 Boss特质需要一个新载体 NewBieBoss,同时实现 Boss 载体的 NewBieBoss 也需要实现 Monster 特质(继承关系组合化), 这部分的代码作为示例让读者理解设定的 Dungeon 模式的行为。

+

接下来,我们可以新增一个新副本,新副本的小怪和 Boss 是新手副本的 NewbieBoss,但是小怪已经不能攻击玩家了(通过动态分发为 dyn Monster 对象来屏蔽 Boss 的攻击行为:

+
struct JuniorDungeonFactory;
+
+impl DungeonFactory for JuniorDungeonFactory {
+    fn create_boss(&self) -> Box<dyn Boss> {
+        NewBieBoss::new()
+    }
+
+    // 新手副本甚至没有小怪,
+    fn create_mosters(&self, amount: usize) -> Vec<Box<dyn Monster>> {
+        vec![NewBieBoss::new(); amount]
+    }
+
+    // generate_dungeon 已有默认实现,不需要修改。
+}
+
+

我们可以看到,抽象工厂模式很方便地为我们重用了已有资源并确保副本的行为效果。

+

为什么使用抽象工厂模式

+
    +
  • 模式统一。通过抽象工厂可以快速地制作符合相同模式,但是资源细节稍有差异的工厂,生成具有同样属性的产品。
  • +
  • 职责专一。每个具体工厂只有一个原因需要被修改——该工厂对应的副本细节变化了。
  • +
  • 符合开闭原则。因为我们统一出了抽象工厂的接口,当我们修改具体工厂的具体实现细节时,并不会影响到接口调用;而新增具体工厂时只需要提供对应的接口,也可以方便地接入副本生成系统。
  • +
+ +
+ +
+ + + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + diff --git a/rust-patterns/builder.html b/rust-patterns/builder.html new file mode 100644 index 0000000..fe88f0c --- /dev/null +++ b/rust-patterns/builder.html @@ -0,0 +1,367 @@ + + + + + + 构建器(Builder)模式 - huangjj27's Tech blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+
+

构建器(Builder)模式

+

示例

+

通常在 Rust 中的实现是通过 不断重建 Builder 来构造最后的类型:

+
struct Counter {
+    counted1: usize,
+    counted2: usize,
+    done: bool,
+}
+
+struct CounterBuilder {
+    counted1: usize,
+    counted2: usize,
+}
+
+impl CounterBuilder {
+    // 构建器需要有默认的参数配置,然后从默认配置触发进行构建。
+    // 不适用 #[derive(std::default::Default)],因为默认配置可能不一样
+    fn default() -> Self {
+        CounterBuiler {
+            counted1: 5,
+            counted2: 0,
+        }
+    }
+
+    // 属性定制方法。消耗原本的构建器,修改属性后重新生成新构建器
+    fn set_counted1(self, cnt: usize) -> Self {
+        self.counted1 = cnt;
+        self
+    }
+
+    fn set_counted2(self, cnt: usize) -> Self {
+        self.counted2 = cnt;
+        self
+    }
+
+    // 最后通过 `build` 方法生成所需类型
+    fn build(self) -> Counter {
+        Counter {
+            counted1: self.counted1,
+            counted2: self.counted2,
+            done: false,
+        }
+    }
+}
+
+

个人实践

+

在设置属性方法的时候,通常的实现是通过消耗原本的构造器后生成新构造器,这使得如果配置构造器的过程不能连续调用属性设置方法时,必须重新捕获构造器:

+
let mut builder = CounterBuilder::default();
+
+// ... 进行一些计算,获得需要配置的值
+let cnt1 = operations();
+
+builder = builder.set_counted1(cnt);
+
+// ... 进行一些计算,获得需要配置的值
+let cnt2 = operations();
+
+builder = builder.set_counted(cnt2);
+
+

以上代码通常出现在需要流计算并及时记录参数配置的时候。并且,如果构造器被更大型的数据结构持有时,消耗并重新构建构造器可能会对性能有点影响。因此在博主个人实现时通常采取传递&mut self 引用的方法来实现属性设置方法:

+
    // ...
+    // 属性定制方法。消耗原本的构建器,修改属性后重新生成新构建器
+    fn set_counted1(&mut self, cnt: usize) -> &mut Self {
+        self.counted1 = cnt;
+        self
+    }
+
+    fn set_counted2(&mut self, cnt: usize) -> &mut Self {
+        self.counted2 = cnt;
+        self
+    }
+
+// ...
+
+

改成如上形式的函数签名,即可 灵活构造 目标结构:

+
let mut builder = CounterBuilder::default();
+
+// ... 进行一些计算,获得需要配置的值
+let cnt1 = operations();
+
+builder.set_counted1(cnt);
+
+// ... 进行一些计算,获得需要配置的值
+let cnt2 = operations();
+
+builder.set_counted(cnt2);
+
+// ... 可能还要等待别的操作完成后再进行构建
+
+let counter = builder.build();
+
+

更好用的工具

+

本文在微信公众号发布之后,微信粉丝 @福糙·仁波切 推荐了 dtolnay/proc-macro-workshop 中的 derive_builder,能够更快地实现自定义的Builder。例如,上文中的示例使用该库可以大大减少代码:

+
use derive_builder::Builder;
+
+struct Counter {
+    #[builder(default = "5")]
+    counted1: usize,
+
+    #[builder(default)]
+    counted2: usize,
+
+    #[builder(default)]
+    done: bool,
+}
+
+
+

甚至可以快速自定义自己的 setter:

+
use derive_builder::Builder;
+
+struct Counter {
+    #[builder(default = "5")]
+    counted1: usize,
+
+    #[builder(default)]
+    counted2: usize,
+
+    #[builder(default)]
+    done: bool,
+}
+
+impl CounterBuilder {
+    fn set_counted2(&mut self, cnt: usize) -> &mut Self {
+        // 注意生成的构造器的字段为 `Option`
+        self.counted2 = Some(if cnt > 100 { 100 } else { cnt });
+        self
+    }
+}
+
+fn main() {
+    let mut builder = CounterBuilder::default();
+
+    builder.set_counted2(123);
+
+    let counter = builder.build();
+
+    println!("{:?}", counter);
+}
+
+
+

为什么使用构造器模式

+
    +
  • 构造过程可控。通常实现构造器模式的时候,我们会将构造器所需要配置的属性设置为私有1,并且只能通过我们提供的属性设置方法进行设置,使得构造过程可控。另外,可以通过属性设置方法提前恐慌(panic)来阻止生成无效对象。
  • +
  • 设置方法职责专一。属性设置方法 职责专一,只会负责设置一种属性,只有在该属性的设置规则改变时,相应的属性设置方法才需要进行修改;
  • +
  • 构造灵活。多个属性设置方法可以自由的组合在一起,也可以分步组合构造。
  • +
  • 可批量构造。我们除了使用消耗性的 build(self) 方法,也可以使用非消耗性的 fn build(&self) 方法,使得构造器可以多次复用。
  • +
  • 符合开闭原则。当某一属性的设置方法内部实现发生变化的时候,不影响其他属性的设置方式;而新增属性及其设置方法时,可以通过链式调用很方便地增加新属性的设置。
  • +
+

为什么不使用构造器模式

+

构造器模式由于有以下缺点而在部分场景中不适用:

+
    +
  • 在构造完成前无法使用被构造对象。在构造完成之前,构造器并不生成被构造对象,因此在整个构造设置完成之前,无法使用被构造对象。
  • +
  • 构造器与被构造对象使用相同的属性设置方法,造成代码重复并无法复用。考虑需要只通过属性设置方法来修改对象的场景,当被构造对象在使用过程中需要频繁设置属性,那么就需要编写对应的属性设置方法;而如果还使用构造器进行对象构造,那么属性设置方法就会重复,并且可能造成构造器与被构造对象的属性设置行为不一致的问题2
  • +
+
1 +

Rust 语言中默认语言项(Item)的可见性都是私有的,如需公开语言项给其他模块使用,需要使用 pub 关键字放开。 +2: 一个绕开的行为不一致问题的方法是将属性设置规则抽取为静态函数,但仍然无法避免过度封装的问题。不过,可以将过度封装的事情交给过程宏等自动代码生成手段,例如文中举例的 derive_builder

+
+ +
+ +
+ + + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + diff --git a/rust-patterns/intro.html b/rust-patterns/intro.html new file mode 100644 index 0000000..404bf41 --- /dev/null +++ b/rust-patterns/intro.html @@ -0,0 +1,212 @@ + + + + + + 设计模式在 Rust 中的实践 - huangjj27's Tech blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+
+

设计模式在 Rust 中的实践

+

[设计模式] 在 Rust 中的应用与其在其他语言中的应用有很多不同之处,本系列为个人在使用 Rust 编程的时候遇到的一些设计模式,并结合自己的思考对 Rust 编写过程中设计模式有特点地优化。

+

特别提醒: 没有银弹!没有银弹!没有银弹! 所有的设计都是为了解决特定场景下的问题,脱离场景扯设计模式都是耍流氓!

+ +
+ +
+ + + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + diff --git a/rust-safe-apps-51.html b/rust-safe-apps-51.html new file mode 100644 index 0000000..adb4edd --- /dev/null +++ b/rust-safe-apps-51.html @@ -0,0 +1,265 @@ + + + + + + Rust 安全应用开发51条 - huangjj27's Tech blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+
+

Rust 安全应用开发51条

+
+

本文摘自法国国家网络安全局(ASSNI)的《用 Rust 开发安全应用的编程规则》(PROGRAMMING RULES TO DEVELOPSECURE APPLICATIONS WITH RUST

+
+
    +
  1. 要使用 stable 编译工具链
  2. +
  3. 要在 cargo 配置文件中将重要变量保持为默认值
  4. +
  5. 要在运行 cargo 时保持编译环境变量为默认值
  6. +
  7. 要周期地使用 linter
  8. +
  9. 要使用 Rust 格式器(rustfmt)
  10. +
  11. 要人工检查自动修复
  12. +
  13. 要检查依赖版本是否过期(cargo-outdated)
  14. +
  15. 要检查依赖的安全脆弱性(vulnerabilities)(cargo-audit)
  16. +
  17. 要遵循命名转换
  18. +
  19. 不要使用 unsafe
  20. +
  21. 要用合适的算术操作来处理潜在的溢出
  22. +
  23. 推荐实现包含了所有可能错误的自定义错误类型
  24. +
  25. 推荐使用 ? 操作符且不使用 try!
  26. +
  27. 不要使用能导致 panic! 的函数
  28. +
  29. 要测试数组索引使用是否正确,或者使用 get 方法
  30. +
  31. 要在 FFI 中正确地处理 panic!
  32. +
  33. 不要使用 forget
  34. +
  35. 推荐使用 clippy 检查 forget 的使用
  36. +
  37. 不要泄露内存
  38. +
  39. 要释放包裹在 ManaullyDrop 里的值
  40. +
  41. 总是调用 into_rawed 值对应的 from_raw 函数
  42. +
  43. 不要使用未初始化内存
  44. +
  45. 使用完敏感数据后要将内存清零
  46. +
  47. 推荐校验 Drop 实现
  48. +
  49. 不要再 Drop 实现内部恐慌(panic)
  50. +
  51. 不允许 Drop 的循环引用
  52. +
  53. 推荐不依赖 Drop 来保证安全
  54. +
  55. 推荐校验 SendSync 实现
  56. +
  57. 要遵循标准库比较特质(trait)的不变之处
  58. +
  59. 推荐使用标准库比较特质的默认实现
  60. +
  61. 推荐尽可能派生(derive)比较特质
  62. +
  63. 要在 FFI 中只使用 C 兼容类型
  64. +
  65. 在 FFI 边界要使用兼容性的类型
  66. +
  67. 推荐使用绑定自动生成工具
  68. +
  69. 在绑定到平台依赖类型时,要使用可移植别名 c_*
  70. +
  71. 推荐在 Rust 中检查外部类型
  72. +
  73. 推荐指针类型而不是引用类型1
  74. +
  75. 不要使用未检查的外部引用
  76. +
  77. 检查外部指针
  78. +
  79. 要标记 FFI 中的函数指针类型为 externunsafe
  80. +
  81. 检查外部函数指针
  82. +
  83. 建议不在 FFI 边界使用不美容的 Rust enum 类型
  84. +
  85. 建议为外部不透明类型使用专门的 Rust 类型
  86. +
  87. 推荐使用不完整的 C/C++ struct 指针来使得类型不透明
  88. +
  89. 不要在 FFI 边界使用实现了 Drop 的类型
  90. +
  91. 要确保在 FFI 中清除数据所有权
  92. +
  93. 推荐将外部数据包裹在可释放内存的包装类型
  94. +
  95. 要在 FFI 中 正确地处理 panic!
  96. +
  97. 推荐为外部库提供安全的包装
  98. +
  99. 推荐只暴露专门的 C 兼容 API
  100. +
+ +
+ +
+ + + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + diff --git a/rust-tianhe-ii.html b/rust-tianhe-ii.html new file mode 100644 index 0000000..29a96a0 --- /dev/null +++ b/rust-tianhe-ii.html @@ -0,0 +1,308 @@ + + + + + + 在天河二号上配置 Rust 运行环境 - huangjj27's Tech blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+
+

在天河二号上配置 Rust 运行环境

+
+

受朋友委托,需要帮忙在“天河二号”超级计算机上配置 Rust 编程语言运行环境,并配置安装 rust-overlaps

+
+

阅读须知

+

本文将不涉及:

+ +

通过 Rust 独立包安装适合 天河二号的 Rust 运行环境

+
    +
  1. ssh 远程登录到天河二号1: +
    $ ssh -i${YOUR_CERTIFICATE_ID} -P${SSH_PORT} ${YOUR_USERNAME}@server.ip.in.vpn
    +
    +
  2. +
  3. 获取超算的服务器平台架构: +
    [you@tainhe2-H ~]$ uname -r
    +
    +
  4. +
  5. 了解平台架构后,获取对应平台的Rust 独立安装包, 并上传至超算。此处以x86_64架构为例: +
    $ scp -i${YOUR_CERTIFICATE_ID} -P${SSH_PORT} rust-1.44.0-x86_64-unknown-linux-gnu.tar.gz you@server.ip.in.vpn:~
    +
    +
  6. +
  7. 解压安装压缩包: +
    [you@tainhe2-H ~]$ tar -zxvf rust-1.44.0-x86_64-unknown-linux-gnu.tar.gz
    +
    +
  8. +
  9. 切换到解压缩目录,并执行安装命令: +
    [you@tainhe2-H ~]$ cd rust-1.44.0-x86_64-unknown-linux-gnu
    +[you@tainhe2-H rust-1.44.0-x86_64-unknown-linux-gnu]$ ./install.sh --prefix=~/rust --disable-ldconfig --verbose
    +
    +此命令会将 Rust 安装在 ~/rust 文件夹中,rust 的 可执行文件将会放在 ~/rust/bin文件夹中。
  10. +
  11. 编辑~/.bashrc, 增加下面这一行配置: +
    export PATH=$HOME/rust/bin:$PATH
    +
    +
  12. +
  13. 使~/.bashrc生效: +
    [you@tainhe2-H ~]$ source ~/.bashrc
    +
    +
  14. +
  15. 检查 Rust 是否成功安装: +
    [you@tainhe2-H ~]$ cargo --version
    +cargo 1.44.0 (05d080faa 2020-05-06)
    +
    +
  16. +
+

离线安装 rust-overlaps

+
    +
  1. 在本地联网环境拷贝源代码: +
    git clone https://github.com/sirkibsirkib/rust-overlaps.git
    +
    +
  2. +
  3. 修复源码的 Cargo.tomlversion2: +
    version = "1.1.0"
    +
    +
  4. +
  5. 在代码仓库目录下执行 cargo vendor,获取依赖的源码3: +
    rust-overlaps$ cargo vendor --respect-source-config
    +
    +下载好的依赖将会存放到 vendor文件夹中。
  6. +
  7. rust-overlaps 文件夹中添加 .cargo/config 文件,以便在超算的离线环境中使用本地缓存好的依赖源码进行编译: +
    [source.crates-io]
    +replace-with = "vendored-sources"
    +
    +[source.vendored-sources]
    +directory = "vendor"
    +
    +
  8. +
  9. 将源码文件夹打包成 .zip 包,然后上传到超算: +
    $ scp -i${YOUR_CERTIFICATE_ID} -P${SSH_PORT} rust-overlaps.zip you@server.ip.in.vpn:~
    +
    +
  10. +
  11. 在超算中解压: +
    [you@tainhe2-H ~]$ unzip rust-overlaps.zip
    +
    +
  12. +
  13. 离线安装3: +
    [you@tainhe2-H ~]$ cd rust-overlaps
    +[you@tainhe2-H rust-overlaps]$ cargo install --path . --offline
    +
    +
  14. +
  15. 检查是否安装成功: +
    [you@tainhe2-H ~]$ rust-overlaps --version
    +ASPOPsolver 1.0
    +
    +
  16. +
+

小结

+

当我看到 rust-overlaps 已经超过三年没有更新之后,我就觉得很可能不能够成功编译——但是 Rust 从来没有让我失望 —— 在本文中,我们使用的是最新稳定版的 Rust 1.44, 然而编译一个三年的旧库一次就可以编译成功了。同样,得益于 Rust 以 crate 为单位的并行与增量编译,让编译命令中断后可以继续执行而不需从头编译。这个故事告诉我们,充分吸收现代学术成果的工具比起偏旧的工具对于效率提高有重要影响!

+
1 +

ssh登陆前还需登录VPN环境,账号密码为管理员提供的账号密码。

+
+
2 +

Rust仓库的版本号遵循语义化版本,因此必须为x.y.z的形式。更多参见sirkibsirkib/rust-overlaps#2

+
+
3 +

cargo编译中断,可以重新运行命令继续安装,直到安装完成。

+
+ +
+ +
+ + + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + diff --git a/searcher.js b/searcher.js new file mode 100644 index 0000000..d2b0aee --- /dev/null +++ b/searcher.js @@ -0,0 +1,483 @@ +"use strict"; +window.search = window.search || {}; +(function search(search) { + // Search functionality + // + // You can use !hasFocus() to prevent keyhandling in your key + // event handlers while the user is typing their search. + + if (!Mark || !elasticlunr) { + return; + } + + //IE 11 Compatibility from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/startsWith + if (!String.prototype.startsWith) { + String.prototype.startsWith = function(search, pos) { + return this.substr(!pos || pos < 0 ? 0 : +pos, search.length) === search; + }; + } + + var search_wrap = document.getElementById('search-wrapper'), + searchbar = document.getElementById('searchbar'), + searchbar_outer = document.getElementById('searchbar-outer'), + searchresults = document.getElementById('searchresults'), + searchresults_outer = document.getElementById('searchresults-outer'), + searchresults_header = document.getElementById('searchresults-header'), + searchicon = document.getElementById('search-toggle'), + content = document.getElementById('content'), + + searchindex = null, + doc_urls = [], + results_options = { + teaser_word_count: 30, + limit_results: 30, + }, + search_options = { + bool: "AND", + expand: true, + fields: { + title: {boost: 1}, + body: {boost: 1}, + breadcrumbs: {boost: 0} + } + }, + mark_exclude = [], + marker = new Mark(content), + current_searchterm = "", + URL_SEARCH_PARAM = 'search', + URL_MARK_PARAM = 'highlight', + teaser_count = 0, + + SEARCH_HOTKEY_KEYCODE = 83, + ESCAPE_KEYCODE = 27, + DOWN_KEYCODE = 40, + UP_KEYCODE = 38, + SELECT_KEYCODE = 13; + + function hasFocus() { + return searchbar === document.activeElement; + } + + function removeChildren(elem) { + while (elem.firstChild) { + elem.removeChild(elem.firstChild); + } + } + + // Helper to parse a url into its building blocks. + function parseURL(url) { + var a = document.createElement('a'); + a.href = url; + return { + source: url, + protocol: a.protocol.replace(':',''), + host: a.hostname, + port: a.port, + params: (function(){ + var ret = {}; + var seg = a.search.replace(/^\?/,'').split('&'); + var len = seg.length, i = 0, s; + for (;i': '>', + '"': '"', + "'": ''' + }; + var repl = function(c) { return MAP[c]; }; + return function(s) { + return s.replace(/[&<>'"]/g, repl); + }; + })(); + + function formatSearchMetric(count, searchterm) { + if (count == 1) { + return count + " search result for '" + searchterm + "':"; + } else if (count == 0) { + return "No search results for '" + searchterm + "'."; + } else { + return count + " search results for '" + searchterm + "':"; + } + } + + function formatSearchResult(result, searchterms) { + var teaser = makeTeaser(escapeHTML(result.doc.body), searchterms); + teaser_count++; + + // The ?URL_MARK_PARAM= parameter belongs inbetween the page and the #heading-anchor + var url = doc_urls[result.ref].split("#"); + if (url.length == 1) { // no anchor found + url.push(""); + } + + // encodeURIComponent escapes all chars that could allow an XSS except + // for '. Due to that we also manually replace ' with its url-encoded + // representation (%27). + var searchterms = encodeURIComponent(searchterms.join(" ")).replace(/\'/g, "%27"); + + return '' + result.doc.breadcrumbs + '' + + '' + + teaser + ''; + } + + function makeTeaser(body, searchterms) { + // The strategy is as follows: + // First, assign a value to each word in the document: + // Words that correspond to search terms (stemmer aware): 40 + // Normal words: 2 + // First word in a sentence: 8 + // Then use a sliding window with a constant number of words and count the + // sum of the values of the words within the window. Then use the window that got the + // maximum sum. If there are multiple maximas, then get the last one. + // Enclose the terms in . + var stemmed_searchterms = searchterms.map(function(w) { + return elasticlunr.stemmer(w.toLowerCase()); + }); + var searchterm_weight = 40; + var weighted = []; // contains elements of ["word", weight, index_in_document] + // split in sentences, then words + var sentences = body.toLowerCase().split('. '); + var index = 0; + var value = 0; + var searchterm_found = false; + for (var sentenceindex in sentences) { + var words = sentences[sentenceindex].split(' '); + value = 8; + for (var wordindex in words) { + var word = words[wordindex]; + if (word.length > 0) { + for (var searchtermindex in stemmed_searchterms) { + if (elasticlunr.stemmer(word).startsWith(stemmed_searchterms[searchtermindex])) { + value = searchterm_weight; + searchterm_found = true; + } + }; + weighted.push([word, value, index]); + value = 2; + } + index += word.length; + index += 1; // ' ' or '.' if last word in sentence + }; + index += 1; // because we split at a two-char boundary '. ' + }; + + if (weighted.length == 0) { + return body; + } + + var window_weight = []; + var window_size = Math.min(weighted.length, results_options.teaser_word_count); + + var cur_sum = 0; + for (var wordindex = 0; wordindex < window_size; wordindex++) { + cur_sum += weighted[wordindex][1]; + }; + window_weight.push(cur_sum); + for (var wordindex = 0; wordindex < weighted.length - window_size; wordindex++) { + cur_sum -= weighted[wordindex][1]; + cur_sum += weighted[wordindex + window_size][1]; + window_weight.push(cur_sum); + }; + + if (searchterm_found) { + var max_sum = 0; + var max_sum_window_index = 0; + // backwards + for (var i = window_weight.length - 1; i >= 0; i--) { + if (window_weight[i] > max_sum) { + max_sum = window_weight[i]; + max_sum_window_index = i; + } + }; + } else { + max_sum_window_index = 0; + } + + // add around searchterms + var teaser_split = []; + var index = weighted[max_sum_window_index][2]; + for (var i = max_sum_window_index; i < max_sum_window_index+window_size; i++) { + var word = weighted[i]; + if (index < word[2]) { + // missing text from index to start of `word` + teaser_split.push(body.substring(index, word[2])); + index = word[2]; + } + if (word[1] == searchterm_weight) { + teaser_split.push("") + } + index = word[2] + word[0].length; + teaser_split.push(body.substring(word[2], index)); + if (word[1] == searchterm_weight) { + teaser_split.push("") + } + }; + + return teaser_split.join(''); + } + + function init(config) { + results_options = config.results_options; + search_options = config.search_options; + searchbar_outer = config.searchbar_outer; + doc_urls = config.doc_urls; + searchindex = elasticlunr.Index.load(config.index); + + // Set up events + searchicon.addEventListener('click', function(e) { searchIconClickHandler(); }, false); + searchbar.addEventListener('keyup', function(e) { searchbarKeyUpHandler(); }, false); + document.addEventListener('keydown', function(e) { globalKeyHandler(e); }, false); + // If the user uses the browser buttons, do the same as if a reload happened + window.onpopstate = function(e) { doSearchOrMarkFromUrl(); }; + // Suppress "submit" events so the page doesn't reload when the user presses Enter + document.addEventListener('submit', function(e) { e.preventDefault(); }, false); + + // If reloaded, do the search or mark again, depending on the current url parameters + doSearchOrMarkFromUrl(); + } + + function unfocusSearchbar() { + // hacky, but just focusing a div only works once + var tmp = document.createElement('input'); + tmp.setAttribute('style', 'position: absolute; opacity: 0;'); + searchicon.appendChild(tmp); + tmp.focus(); + tmp.remove(); + } + + // On reload or browser history backwards/forwards events, parse the url and do search or mark + function doSearchOrMarkFromUrl() { + // Check current URL for search request + var url = parseURL(window.location.href); + if (url.params.hasOwnProperty(URL_SEARCH_PARAM) + && url.params[URL_SEARCH_PARAM] != "") { + showSearch(true); + searchbar.value = decodeURIComponent( + (url.params[URL_SEARCH_PARAM]+'').replace(/\+/g, '%20')); + searchbarKeyUpHandler(); // -> doSearch() + } else { + showSearch(false); + } + + if (url.params.hasOwnProperty(URL_MARK_PARAM)) { + var words = decodeURIComponent(url.params[URL_MARK_PARAM]).split(' '); + marker.mark(words, { + exclude: mark_exclude + }); + + var markers = document.querySelectorAll("mark"); + function hide() { + for (var i = 0; i < markers.length; i++) { + markers[i].classList.add("fade-out"); + window.setTimeout(function(e) { marker.unmark(); }, 300); + } + } + for (var i = 0; i < markers.length; i++) { + markers[i].addEventListener('click', hide); + } + } + } + + // Eventhandler for keyevents on `document` + function globalKeyHandler(e) { + if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey || e.target.type === 'textarea' || e.target.type === 'text') { return; } + + if (e.keyCode === ESCAPE_KEYCODE) { + e.preventDefault(); + searchbar.classList.remove("active"); + setSearchUrlParameters("", + (searchbar.value.trim() !== "") ? "push" : "replace"); + if (hasFocus()) { + unfocusSearchbar(); + } + showSearch(false); + marker.unmark(); + } else if (!hasFocus() && e.keyCode === SEARCH_HOTKEY_KEYCODE) { + e.preventDefault(); + showSearch(true); + window.scrollTo(0, 0); + searchbar.select(); + } else if (hasFocus() && e.keyCode === DOWN_KEYCODE) { + e.preventDefault(); + unfocusSearchbar(); + searchresults.firstElementChild.classList.add("focus"); + } else if (!hasFocus() && (e.keyCode === DOWN_KEYCODE + || e.keyCode === UP_KEYCODE + || e.keyCode === SELECT_KEYCODE)) { + // not `:focus` because browser does annoying scrolling + var focused = searchresults.querySelector("li.focus"); + if (!focused) return; + e.preventDefault(); + if (e.keyCode === DOWN_KEYCODE) { + var next = focused.nextElementSibling; + if (next) { + focused.classList.remove("focus"); + next.classList.add("focus"); + } + } else if (e.keyCode === UP_KEYCODE) { + focused.classList.remove("focus"); + var prev = focused.previousElementSibling; + if (prev) { + prev.classList.add("focus"); + } else { + searchbar.select(); + } + } else { // SELECT_KEYCODE + window.location.assign(focused.querySelector('a')); + } + } + } + + function showSearch(yes) { + if (yes) { + search_wrap.classList.remove('hidden'); + searchicon.setAttribute('aria-expanded', 'true'); + } else { + search_wrap.classList.add('hidden'); + searchicon.setAttribute('aria-expanded', 'false'); + var results = searchresults.children; + for (var i = 0; i < results.length; i++) { + results[i].classList.remove("focus"); + } + } + } + + function showResults(yes) { + if (yes) { + searchresults_outer.classList.remove('hidden'); + } else { + searchresults_outer.classList.add('hidden'); + } + } + + // Eventhandler for search icon + function searchIconClickHandler() { + if (search_wrap.classList.contains('hidden')) { + showSearch(true); + window.scrollTo(0, 0); + searchbar.select(); + } else { + showSearch(false); + } + } + + // Eventhandler for keyevents while the searchbar is focused + function searchbarKeyUpHandler() { + var searchterm = searchbar.value.trim(); + if (searchterm != "") { + searchbar.classList.add("active"); + doSearch(searchterm); + } else { + searchbar.classList.remove("active"); + showResults(false); + removeChildren(searchresults); + } + + setSearchUrlParameters(searchterm, "push_if_new_search_else_replace"); + + // Remove marks + marker.unmark(); + } + + // Update current url with ?URL_SEARCH_PARAM= parameter, remove ?URL_MARK_PARAM and #heading-anchor . + // `action` can be one of "push", "replace", "push_if_new_search_else_replace" + // and replaces or pushes a new browser history item. + // "push_if_new_search_else_replace" pushes if there is no `?URL_SEARCH_PARAM=abc` yet. + function setSearchUrlParameters(searchterm, action) { + var url = parseURL(window.location.href); + var first_search = ! url.params.hasOwnProperty(URL_SEARCH_PARAM); + if (searchterm != "" || action == "push_if_new_search_else_replace") { + url.params[URL_SEARCH_PARAM] = searchterm; + delete url.params[URL_MARK_PARAM]; + url.hash = ""; + } else { + delete url.params[URL_MARK_PARAM]; + delete url.params[URL_SEARCH_PARAM]; + } + // A new search will also add a new history item, so the user can go back + // to the page prior to searching. A updated search term will only replace + // the url. + if (action == "push" || (action == "push_if_new_search_else_replace" && first_search) ) { + history.pushState({}, document.title, renderURL(url)); + } else if (action == "replace" || (action == "push_if_new_search_else_replace" && !first_search) ) { + history.replaceState({}, document.title, renderURL(url)); + } + } + + function doSearch(searchterm) { + + // Don't search the same twice + if (current_searchterm == searchterm) { return; } + else { current_searchterm = searchterm; } + + if (searchindex == null) { return; } + + // Do the actual search + var results = searchindex.search(searchterm, search_options); + var resultcount = Math.min(results.length, results_options.limit_results); + + // Display search metrics + searchresults_header.innerText = formatSearchMetric(resultcount, searchterm); + + // Clear and insert results + var searchterms = searchterm.split(' '); + removeChildren(searchresults); + for(var i = 0; i < resultcount ; i++){ + var resultElem = document.createElement('li'); + resultElem.innerHTML = formatSearchResult(results[i], searchterms); + searchresults.appendChild(resultElem); + } + + // Display results + showResults(true); + } + + fetch(path_to_root + 'searchindex.json') + .then(response => response.json()) + .then(json => init(json)) + .catch(error => { // Try to load searchindex.js if fetch failed + var script = document.createElement('script'); + script.src = path_to_root + 'searchindex.js'; + script.onload = () => init(window.search); + document.head.appendChild(script); + }); + + // Exported functions + search.hasFocus = hasFocus; +})(window.search); diff --git a/searchindex.js b/searchindex.js new file mode 100644 index 0000000..c8fa14e --- /dev/null +++ b/searchindex.js @@ -0,0 +1 @@ +Object.assign(window.search, {"doc_urls":["tauri-single-instance-bug-dangling.html#由-tauri-单例模式-bug-意外修复-发现的-dangling","tauri-single-instance-bug-dangling.html#tldr","tauri-single-instance-bug-dangling.html#所以这个-bug-的现象是怎么样的","tauri-single-instance-bug-dangling.html#potatotoolarge-传递给-win32-api-的字符串要用-c-string-风格的-0-结束","tauri-single-instance-bug-dangling.html#但它并不是真的修复","tauri-single-instance-bug-dangling.html#那么失效的过程发生了什么","tauri-single-instance-bug-dangling.html#一语惊醒梦中人悬垂指针dangling","tauri-single-instance-bug-dangling.html#那为什么在-format-的时候手动添加-0-后问题修复了呢","tauri-single-instance-bug-dangling.html#教训与经验","load-wasm-mistake.html#尝试在单-html-文件中嵌入-wasm-模块的错误操作","load-wasm-mistake.html#tldr","load-wasm-mistake.html#项目背景","load-wasm-mistake.html#demo-项目架构","load-wasm-mistake.html#薛定谔的-javascript-函数","load-wasm-mistake.html#然而我还是想包容你的我的-wasm","reservoir.html#蓄水池算法改进---面向抽奖场景保证等概率性","reservoir.html#算法描述与等概率性证明","reservoir.html#抽奖人数不足时-n-lt-m--","reservoir.html#抽奖人数足够多时-n-gt-m--","reservoir.html#整合后的算法","reservoir.html#程序实现","reservoir.html#rust","reservoir.html#建模与实现","reservoir.html#优点","reservoir.html#缺点","reservoir.html#下一步可能展开的工作","reservoir.html#致谢","interview.html#总结一次面试","interview.html#最值得总结的三个问题","interview.html#线程同步有哪些方法如何用这些方法实现一个-rwlock","interview.html#有什么问题是生存期标注无法修正的请给出一个例子","interview.html#waker-如何被唤醒-reactor要怎么实现","interview.html#一面----手写代码","interview.html#1-实现一个二分查找函数","interview.html#2-镜像二叉树","snake-with-bevy.html#用-bevy-游戏引擎编写贪吃蛇译","snake-with-bevy.html#新的空的-bevy-应用","snake-with-bevy.html#创建窗口","snake-with-bevy.html#开始编写一条蛇","snake-with-bevy.html#移动小蛇","snake-with-bevy.html#控制小蛇","snake-with-bevy.html#码格子","snake-with-bevy.html#使用我们的格子","snake-with-bevy.html#调整窗口大小","snake-with-bevy.html#生成食物","snake-with-bevy.html#更像蛇的移动","snake-with-bevy.html#加个尾巴","snake-with-bevy.html#让尾巴跟着小蛇活动","snake-with-bevy.html#小蛇成长","snake-with-bevy.html#撞墙或者咬尾巴","rust-safe-apps-51.html#rust-安全应用开发51条","rust-tianhe-ii.html#在天河二号上配置-rust-运行环境","rust-tianhe-ii.html#阅读须知","rust-tianhe-ii.html#通过-rust-独立包安装适合-天河二号的-rust-运行环境","rust-tianhe-ii.html#离线安装-rust-overlaps","rust-tianhe-ii.html#小结","rust-patterns/intro.html#设计模式在-rust-中的实践","rust-patterns/builder.html#构建器builder模式","rust-patterns/builder.html#示例","rust-patterns/builder.html#个人实践","rust-patterns/builder.html#更好用的工具","rust-patterns/builder.html#为什么使用构造器模式","rust-patterns/builder.html#为什么不使用构造器模式","rust-patterns/abstract-factory.html#抽象工厂abstract-factory模式","rust-patterns/abstract-factory.html#场景需求","rust-patterns/abstract-factory.html#场景设计","rust-patterns/abstract-factory.html#为什么使用抽象工厂模式","rust-mirror.html#rust-镜像","rust-mirror.html#阅读须知","rust-mirror.html#使用国内镜像加速更新-rustup-工具链","rust-mirror.html#使用国内镜像加速更新-crate-拉取","rust-mirror.html#参考资料","rust-ffi.html#在-wsl-中学习-rust-ffi","rust-ffi.html#阅读须知","rust-ffi.html#wsl-rust-环境搭建","rust-ffi.html#rust-调用-cc","rust-ffi.html#在-cc-中调用-rust","rust-ffi.html#小结","wasi/intro.html","wasi/wasi_and_wasmtime.html#wasi探索一----wasi简介与wasmtime配置","wasi/wasi_and_wasmtime.html#什么是wasi","wasi/wasi_and_wasmtime.html#关于wasm-runtime","wasi/wasi_and_wasmtime.html#wasmtime与rust环境配置","wasi/wasi_and_wasmtime.html#配置rust","wasi/wasi_and_wasmtime.html#配置wasmtime","wasi/wasi_and_wasmtime.html#试验","wasi/wasi_guess.html#wasi探索二----wasi版猜数字","wasi/wasi_guess.html#阅读须知","wasi/wasi_guess.html#原版猜数字","wasi/wasi_guess.html#一次游戏只猜一个数","wasi/wasi_guess.html#加上log追踪生成的数据情况","wasi/wasi_guess.html#向高难度挑战","wasi/wasi_guess.html#完整代码","wasi/wasi_guess.html#是时候编译成wasi目标了","wasi/wasi_guess.html#wasmer与wapm","number_theory/intro.html#初等数论自我探索","number_theory/if_2n-p_is_divided_by_p.html#若质数p不能整除偶数2n则p不整除2n-p","number_theory/if_2n-p_is_divided_by_p.html#命题形式化描述","number_theory/if_2n-p_is_divided_by_p.html#一般化命题与反证法","number_theory/if_2n-p_is_divided_by_p.html#反证法","number_theory/if_2n-p_is_divided_by_p.html#代入条件","number_theory/if_2n-p_is_divided_by_p.html#一整数若不同时整除互质两数则不能整除后两数之差","number_theory/if_2n-p_is_divided_by_p.html#形式化描述","number_theory/if_2n-p_is_divided_by_p.html#证明","number_theory/if_2n-p_is_divided_by_p.html#代入条件-1","number_theory/single-composite-divsion.html#整数和它两倍一倍半之间的合数可以整除整数的阶乘","number_theory/single-composite-divsion.html#形式化描述","number_theory/single-composite-divsion.html#证明","number_theory/single-composite-divsion.html#充分性","number_theory/single-composite-divsion.html#必要性","number_theory/single-composite-divsion.html#更强的结论","number_theory/goldbachs-conjecture.html#用初等数论探索哥德巴赫猜想","number_theory/goldbachs-conjecture.html#哥德巴赫猜想","number_theory/goldbachs-conjecture.html#引理","number_theory/goldbachs-conjecture.html#一-p中存在不整除n的元素","number_theory/goldbachs-conjecture.html#二-给定正整数-x-若整除互质数对-a-b-之一则-x-不整除-a-b","number_theory/goldbachs-conjecture.html#探索哥德巴赫猜想","number_theory/goldbachs-conjecture.html#进一步探究","number_theory/goldbachs-conjecture.html#推论一","number_theory/goldbachs-conjecture.html#推论二","resume.html#黄俊杰","resume.html#教育背景","resume.html#项目经历","resume.html#数字化营业厅-202109---至今","resume.html#银行业客户经理智能推荐与客户反馈收集项目-202011---202109","resume.html#hiveql-静态代码扫描检查工具-20194","resume.html#客户个人金融业务管理平台-20188----20194","resume.html#rust-中的异步编程","resume.html#env_logger","resume.html#tlssigapi----使用-rust-重写-tencent-login-service-signature-api","resume.html#技能"],"index":{"documentStore":{"docInfo":{"0":{"body":0,"breadcrumbs":6,"title":3},"1":{"body":7,"breadcrumbs":4,"title":1},"10":{"body":8,"breadcrumbs":3,"title":1},"100":{"body":18,"breadcrumbs":2,"title":0},"101":{"body":0,"breadcrumbs":2,"title":0},"102":{"body":26,"breadcrumbs":2,"title":0},"103":{"body":71,"breadcrumbs":2,"title":0},"104":{"body":16,"breadcrumbs":2,"title":0},"105":{"body":0,"breadcrumbs":0,"title":0},"106":{"body":22,"breadcrumbs":0,"title":0},"107":{"body":0,"breadcrumbs":0,"title":0},"108":{"body":125,"breadcrumbs":0,"title":0},"109":{"body":41,"breadcrumbs":0,"title":0},"11":{"body":8,"breadcrumbs":2,"title":0},"110":{"body":32,"breadcrumbs":0,"title":0},"111":{"body":6,"breadcrumbs":0,"title":0},"112":{"body":18,"breadcrumbs":0,"title":0},"113":{"body":0,"breadcrumbs":0,"title":0},"114":{"body":77,"breadcrumbs":1,"title":1},"115":{"body":91,"breadcrumbs":4,"title":4},"116":{"body":73,"breadcrumbs":0,"title":0},"117":{"body":0,"breadcrumbs":0,"title":0},"118":{"body":35,"breadcrumbs":0,"title":0},"119":{"body":95,"breadcrumbs":0,"title":0},"12":{"body":117,"breadcrumbs":3,"title":1},"120":{"body":10,"breadcrumbs":0,"title":0},"121":{"body":2,"breadcrumbs":0,"title":0},"122":{"body":0,"breadcrumbs":0,"title":0},"123":{"body":6,"breadcrumbs":1,"title":1},"124":{"body":6,"breadcrumbs":2,"title":2},"125":{"body":6,"breadcrumbs":2,"title":2},"126":{"body":5,"breadcrumbs":2,"title":2},"127":{"body":12,"breadcrumbs":1,"title":1},"128":{"body":9,"breadcrumbs":1,"title":1},"129":{"body":1,"breadcrumbs":7,"title":7},"13":{"body":125,"breadcrumbs":3,"title":1},"130":{"body":9,"breadcrumbs":0,"title":0},"14":{"body":305,"breadcrumbs":3,"title":1},"15":{"body":55,"breadcrumbs":0,"title":0},"16":{"body":7,"breadcrumbs":0,"title":0},"17":{"body":155,"breadcrumbs":3,"title":3},"18":{"body":18,"breadcrumbs":3,"title":3},"19":{"body":60,"breadcrumbs":0,"title":0},"2":{"body":22,"breadcrumbs":4,"title":1},"20":{"body":0,"breadcrumbs":0,"title":0},"21":{"body":9,"breadcrumbs":1,"title":1},"22":{"body":207,"breadcrumbs":0,"title":0},"23":{"body":0,"breadcrumbs":0,"title":0},"24":{"body":2,"breadcrumbs":0,"title":0},"25":{"body":0,"breadcrumbs":0,"title":0},"26":{"body":3,"breadcrumbs":0,"title":0},"27":{"body":0,"breadcrumbs":0,"title":0},"28":{"body":0,"breadcrumbs":0,"title":0},"29":{"body":47,"breadcrumbs":1,"title":1},"3":{"body":150,"breadcrumbs":9,"title":6},"30":{"body":14,"breadcrumbs":0,"title":0},"31":{"body":18,"breadcrumbs":2,"title":2},"32":{"body":0,"breadcrumbs":0,"title":0},"33":{"body":114,"breadcrumbs":1,"title":1},"34":{"body":222,"breadcrumbs":1,"title":1},"35":{"body":9,"breadcrumbs":2,"title":1},"36":{"body":19,"breadcrumbs":2,"title":1},"37":{"body":22,"breadcrumbs":1,"title":0},"38":{"body":62,"breadcrumbs":1,"title":0},"39":{"body":34,"breadcrumbs":1,"title":0},"4":{"body":52,"breadcrumbs":3,"title":0},"40":{"body":32,"breadcrumbs":1,"title":0},"41":{"body":147,"breadcrumbs":1,"title":0},"42":{"body":27,"breadcrumbs":1,"title":0},"43":{"body":25,"breadcrumbs":1,"title":0},"44":{"body":76,"breadcrumbs":1,"title":0},"45":{"body":114,"breadcrumbs":1,"title":0},"46":{"body":97,"breadcrumbs":1,"title":0},"47":{"body":34,"breadcrumbs":1,"title":0},"48":{"body":74,"breadcrumbs":1,"title":0},"49":{"body":56,"breadcrumbs":1,"title":0},"5":{"body":416,"breadcrumbs":3,"title":0},"50":{"body":59,"breadcrumbs":4,"title":2},"51":{"body":3,"breadcrumbs":2,"title":1},"52":{"body":0,"breadcrumbs":1,"title":0},"53":{"body":77,"breadcrumbs":3,"title":2},"54":{"body":65,"breadcrumbs":3,"title":2},"55":{"body":12,"breadcrumbs":1,"title":0},"56":{"body":3,"breadcrumbs":2,"title":1},"57":{"body":0,"breadcrumbs":3,"title":1},"58":{"body":54,"breadcrumbs":2,"title":0},"59":{"body":43,"breadcrumbs":2,"title":0},"6":{"body":34,"breadcrumbs":4,"title":1},"60":{"body":60,"breadcrumbs":2,"title":0},"61":{"body":4,"breadcrumbs":2,"title":0},"62":{"body":5,"breadcrumbs":2,"title":0},"63":{"body":0,"breadcrumbs":5,"title":2},"64":{"body":1,"breadcrumbs":3,"title":0},"65":{"body":167,"breadcrumbs":3,"title":0},"66":{"body":0,"breadcrumbs":3,"title":0},"67":{"body":3,"breadcrumbs":2,"title":1},"68":{"body":4,"breadcrumbs":1,"title":0},"69":{"body":15,"breadcrumbs":2,"title":1},"7":{"body":5,"breadcrumbs":5,"title":2},"70":{"body":39,"breadcrumbs":2,"title":1},"71":{"body":10,"breadcrumbs":1,"title":0},"72":{"body":12,"breadcrumbs":6,"title":3},"73":{"body":16,"breadcrumbs":3,"title":0},"74":{"body":27,"breadcrumbs":5,"title":2},"75":{"body":234,"breadcrumbs":5,"title":2},"76":{"body":98,"breadcrumbs":5,"title":2},"77":{"body":14,"breadcrumbs":3,"title":0},"78":{"body":6,"breadcrumbs":1,"title":1},"79":{"body":0,"breadcrumbs":4,"title":2},"8":{"body":8,"breadcrumbs":3,"title":0},"80":{"body":13,"breadcrumbs":3,"title":1},"81":{"body":12,"breadcrumbs":4,"title":2},"82":{"body":7,"breadcrumbs":3,"title":1},"83":{"body":47,"breadcrumbs":3,"title":1},"84":{"body":31,"breadcrumbs":3,"title":1},"85":{"body":8,"breadcrumbs":2,"title":0},"86":{"body":0,"breadcrumbs":4,"title":2},"87":{"body":13,"breadcrumbs":2,"title":0},"88":{"body":63,"breadcrumbs":2,"title":0},"89":{"body":92,"breadcrumbs":2,"title":0},"9":{"body":0,"breadcrumbs":4,"title":2},"90":{"body":53,"breadcrumbs":3,"title":1},"91":{"body":36,"breadcrumbs":2,"title":0},"92":{"body":107,"breadcrumbs":2,"title":0},"93":{"body":348,"breadcrumbs":3,"title":1},"94":{"body":6,"breadcrumbs":3,"title":1},"95":{"body":59,"breadcrumbs":0,"title":0},"96":{"body":0,"breadcrumbs":4,"title":2},"97":{"body":17,"breadcrumbs":2,"title":0},"98":{"body":20,"breadcrumbs":2,"title":0},"99":{"body":15,"breadcrumbs":2,"title":0}},"docs":{"0":{"body":"","breadcrumbs":"由 tauri 单例模式 bug “意外修复” 发现的 dangling » 由 tauri 单例模式 bug “意外修复” 发现的 dangling","id":"0","title":"由 tauri 单例模式 bug “意外修复” 发现的 dangling"},"1":{"body":"tauri 单例插件 用于区分单例实例的 productName的过长会导致 单例功能失效 ,博主最初确信 encode_wide 实现有问题,并提交了 修复 。然而在和社区深入研究问题原因后,发现根本原因是使用 encode_wide 转码传参时造成了 dangling 。 PS: 为方便读者理解,博主花费一天时间重新梳理分析步骤,按照演绎法展示定位 bug 地过程,实现发生的分析过程要比博文的过程更加曲折,对分析理解问题无意义因此略过。","breadcrumbs":"由 tauri 单例模式 bug “意外修复” 发现的 dangling » TL;DR","id":"1","title":"TL;DR"},"10":{"body":"当项目是需要 WASM 与 JavaScript 相互交互的时候,请尽可能在统一的 JavaScript 入口中定义所有的功能; wasm-pack 生成的胶水 JavaScript 与 WASM 可以稍作修改即可嵌入到 HTML 文件中。","breadcrumbs":"尝试在单 HTML 文件中嵌入 WASM 模块的错误操作 » TL;DR","id":"10","title":"TL;DR"},"100":{"body":"令 \\( a := p, b = 2n \\), 则显然: \\[ p \\in Primes, p \\lt 2n \\Rightarrow p \\nmid 2n \\Rightarrow p \\nmid 2n - p \\] 即命题 \\( \\ref{1} \\) 得证。 (以下内容为作者之前思考过的另外一个比较迂回的证明方法,为后续叙述的引理)","breadcrumbs":"初等数论自我探索 » 若质数p不能整除偶数2n,则p不整除2n-p » 代入条件","id":"100","title":"代入条件"},"101":{"body":"","breadcrumbs":"初等数论自我探索 » 若质数p不能整除偶数2n,则p不整除2n-p » 一整数若不同时整除互质两数,则不能整除后两数之差","id":"101","title":"一整数若不同时整除互质两数,则不能整除后两数之差"},"102":{"body":"\\[ \\label{1.1} \\tag{1.1} \\forall x \\in Z^+ \\land x > 1, a \\in Z^+ \\land a > 1, b \\in Z^+ \\land b > 1; \\quad (a, b) = 1, x|a \\lor x|b \\Rightarrow x \\nmid a-b \\]","breadcrumbs":"初等数论自我探索 » 若质数p不能整除偶数2n,则p不整除2n-p » 形式化描述","id":"102","title":"形式化描述"},"103":{"body":"不妨设 \\( a \\gt b \\)。易知\\(x|a\\)与\\(x|b\\)不同时成立, 否则\\((a, b) \\ge x\\), 与 \\((a, b) = 1\\) 矛盾. 分类讨论: \\(x|a, , x \\nmid b\\) \\( \\because x|a \\) \\( \\therefore \\exists m \\in Z^+, mx=a. \\) 假设 \\( x|a-b \\), 即 \\( \\exists k \\in Z^+, kx=a-b \\Leftrightarrow b = a-kx = (m-k)x. \\) 若 \\( m = k \\), 则 \\( b = 0 \\), 与 \\( b > 1 \\) 矛盾; 若 \\( m \\neq k \\), 则 \\( \\exists (m-k) \\in Z, b = (m-k)x \\Leftrightarrow x|b \\), 与假设 \\( x \\nmid b \\) 矛盾! 故假设 \\( x|a-b \\) 不成立, \\( x \\nmid a-b. \\) \\(x \\nmid a, , x|b\\). 同理可得: 若\\( x|a-b\\) , 则 \\( a = b + kx = (m+k)x \\Leftrightarrow x|a \\), 与 \\(x \\nmid a\\) 矛盾! 故 \\(x \\nmid a-b \\).","breadcrumbs":"初等数论自我探索 » 若质数p不能整除偶数2n,则p不整除2n-p » 证明","id":"103","title":"证明"},"104":{"body":"令 \\( x := p, a := 2n, b := p \\),显然有\\( p \\mid p \\)。故,当 \\(p \\nmid 2n \\)时, \\( p \\nmid 2n - p \\),即命题 \\( \\ref{1} \\) 得证。 感谢中山大学数学专业的倪秉业师弟指出这个更简洁的证明方式!","breadcrumbs":"初等数论自我探索 » 若质数p不能整除偶数2n,则p不整除2n-p » 代入条件","id":"104","title":"代入条件"},"105":{"body":"","breadcrumbs":"初等数论自我探索 » 整数和它两倍间的合数的性质 » 整数和它两倍(一倍半)之间的合数,可以整除整数的阶乘","id":"105","title":"整数和它两倍(一倍半)之间的合数,可以整除整数的阶乘"},"106":{"body":"\\[ \\label{2.1} \\tag{2.1} \\forall m \\in Z^+, m \\gt 8, n \\in Z^+, m \\lt n \\lt 2m; \\quad n \\not \\in Primes \\Leftrightarrow n \\mid m! \\]","breadcrumbs":"初等数论自我探索 » 整数和它两倍间的合数的性质 » 形式化描述","id":"106","title":"形式化描述"},"107":{"body":"","breadcrumbs":"初等数论自我探索 » 整数和它两倍间的合数的性质 » 证明","id":"107","title":"证明"},"108":{"body":"\\[ \\label{2.1.1} \\tag{2.1.1} n \\not \\in Primes \\Rightarrow n \\mid m! \\] 因为 \\( n \\) 为合数,将其分为完全平方数和不完全平方数两种情况证明。 不完全平方数 不妨设 \\( n = ab (a \\gt b \\ge 2, (a, b) = 1) \\),则 \\(b \\lt a \\lt m \\),证明如下: 若 \\( a \\ge m \\), 则 \\(n = ab \\ge 2a \\ge 2m \\Leftrightarrow n \\ge 2m \\), 这与 \\( n \\lt 2m \\) 矛盾! 故假设不成立,故 \\(2 \\le b \\lt a \\lt m \\Rightarrow a \\mid m!, b \\mid m! \\)。 又 \\( (a, b) = 1 \\Rightarrow [a, b] = ab = n \\) 因此:\\( a \\mid m!, b \\mid m \\Rightarrow [a, b] \\mid m! \\Leftrightarrow n \\mid m! \\)。 完全平方数 不妨设 \\( n = k^2 \\),则 \\(2 \\lt k \\lt 2k \\lt m \\),证明如下: 若 \\( 2k \\ge m \\),则 \\( k \\ge { m \\over 2 } \\), 则 \\(n = k^2 \\ge { m^2 \\over 4 } \\ge { 8 / 4 } \\cdot m = 2m \\Leftrightarrow n \\ge 2m \\), 这与 \\( n \\lt 2m \\) 矛盾! 故假设不成立,故 \\( 2 \\le k \\lt 2k \\lt m \\Rightarrow 2k^2 \\mid m! \\)。 因此:\\( 2k^2 \\mid m! \\Leftrightarrow 2n | m! \\Rightarrow n \\mid m! \\)。 综上,命题 \\( \\ref{2.1.1} \\) 得证。","breadcrumbs":"初等数论自我探索 » 整数和它两倍间的合数的性质 » 充分性","id":"108","title":"充分性"},"109":{"body":"\\[ \\label{2.1.2} \\tag{2.1.2} n \\mid m! \\Rightarrow n \\not \\in Primes \\] 反证:假设 \\(n \\in Primes \\),则: \\( \\because n > m, n \\in Primes, \\) \\( \\therefore n \\nmid 2, \\space n \\nmid 3, \\space n \\nmid 4, \\space \\dots, \\space n \\mid m \\Rightarrow n \\nmid m!, \\) 这与条件 \\( n \\mid m! \\) 矛盾!故假设不成立,命题 \\( \\ref{2.1.2} \\) 得证。 综上,命题 \\( \\ref{2.1} \\) 得证。","breadcrumbs":"初等数论自我探索 » 整数和它两倍间的合数的性质 » 必要性","id":"109","title":"必要性"},"11":{"body":"当博主兴高采烈地使用 HTML 与 JavaScript 迅速开发好 UI 界面与交互功能的时候,发现核心的功能的 JavaScript 库只支持 npm 环境而无法应用到前述 UI 界面上,博主迫于无奈只能抓起以前做过的 Rust 版本库,尝试改造成 WASM 模块以复用界面代码。因为博主的这个项目是属于不对外开放的项目,因此本文中使用的项目是简化后的 demo,但不影响博主记录以及提醒上述遇到的两个问题(这两个坑每一个都坑掉了我几个小时,但愿会有读者看到我这篇文章抢救一下自己的时间)。","breadcrumbs":"尝试在单 HTML 文件中嵌入 WASM 模块的错误操作 » 项目背景","id":"11","title":"项目背景"},"110":{"body":"显然对 \\( \\forall n \\in (m, 3/2m) \\Rightarrow n \\in (m, 2n) \\),故: \\[ \\label{2.2} \\tag{2.2} \\forall m \\in Z^+, m \\gt 8, n \\in Z^+, m \\lt n \\lt { 3m \\over 2 }; \\quad n \\not \\in Primes \\Leftrightarrow n \\mid m! \\]","breadcrumbs":"初等数论自我探索 » 整数和它两倍间的合数的性质 » 更强的结论","id":"110","title":"更强的结论"},"111":{"body":"在探索哥德巴赫猜想在初等数论框架内证明方式, 并由此发现一些显而易见的有趣结论: 若一个偶数2n能够拆分为两个奇素数的和的形式, 并且如果两个奇素数不相等, 那么这两个素数中较小的一个p(易知\\( p \\lt n \\))必然不能整除n.","breadcrumbs":"初等数论自我探索 » 用初等数论探索哥德巴赫猜想 » 用初等数论探索哥德巴赫猜想","id":"111","title":"用初等数论探索哥德巴赫猜想"},"112":{"body":"关于历史研究历程一类资料, 请参考 wiki . 本文将哥德巴赫猜想简单地描述为: 给定任意整数\\(n(n > 1)\\), 以及不超过n的所有素数 [1] 的集合\\( P = \\{ p \\mid Prime(p) \\land p \\lt n \\} \\). 设\\(p\\)为集合\\( P \\)中的一个元素, 猜想\\(p\\)对应的整数\\(2n-p\\)所组成的集合\\(2n-P\\)中, 必然存在素数元素.","breadcrumbs":"初等数论自我探索 » 用初等数论探索哥德巴赫猜想 » 哥德巴赫猜想","id":"112","title":"哥德巴赫猜想"},"113":{"body":"","breadcrumbs":"初等数论自我探索 » 用初等数论探索哥德巴赫猜想 » 引理","id":"113","title":"引理"},"114":{"body":"引理1: \\[ \\label{1.1} \\tag{1.1} \\exists p \\in P, p \\nmid n \\] 证明(反证法): 假设命题\\( \\ref{1.1} \\) 的反命题: \\[\\label{1.2} \\tag{1.2} \\forall p \\in P, p \\mid n \\Leftrightarrow \\forall p \\in P, \\exists m \\in Z, m \\neq 0 \\land mp = n \\] 成立, 由伯特兰-切比雪夫定理( Bertrand's postulate )可知: \\[ \\exists p_0, {n \\over 2} \\lt p_0 \\lt n \\] 由假设\\( \\ref{1.2} \\) 可得: \\[{mp_0 \\over 2} \\lt p_0 \\Rightarrow m \\lt 2\\]. 又\\(m \\in Z\\), \\(p_0 \\gt 0, n \\gt 0 \\Rightarrow m = {n \\over p_0} \\gt 0\\), 所以\\(m = 1\\), 即\\(mp_0 = p_0 = n\\), 这与\\(p_0 \\lt n\\) 矛盾! 所以命题\\( \\ref{1.2} \\)不成立. 故命题\\( \\ref{1.1} \\)成立.","breadcrumbs":"初等数论自我探索 » 用初等数论探索哥德巴赫猜想 » (一) P中存在不整除n的元素","id":"114","title":"(一) P中存在不整除n的元素"},"115":{"body":"\\[ \\label{2} \\tag{2} (a, b) = 1, x \\gt 1, a \\gt 1, b \\gt 1, x|a \\lor x|b \\Rightarrow x \\nmid a-b \\] 证明: 易知\\(x|a\\)与\\(x|b\\)不同时成立, 否则\\((a, b) \\ge x\\), 与 \\((a, b) = 1\\) 矛盾. 分类讨论: \\(x|a, , x \\nmid b\\) \\( \\because x|a \\) \\( \\therefore \\exists m \\ge 1, m \\in Z, mx=a. \\) 假设 \\( x|a-b \\), 即 \\( \\exists k \\in Z, kx=a-b \\Leftrightarrow b = a-kx = (m-k)x. \\) 若 \\( m = k \\), 则 \\( b = 0 \\), 与 \\( b > 1 \\) 矛盾; 若 \\( m \\neq k \\), 则 \\( \\exists (m-k) \\in Z, b = (m-k)x \\Leftrightarrow x|b \\), 与假设 \\( x \\nmid b \\) 矛盾! 故假设 \\( x|a-b \\) 不成立, \\( x \\nmid a-b. \\) \\(x \\nmid a, , x|b\\). 同理可得: 若\\( x|a-b\\) , 则 \\( a = b + kx = (m+k)x \\Leftrightarrow x|a \\), 与 \\(x \\nmid a\\) 矛盾! 故 \\(x \\nmid a-b \\).","breadcrumbs":"初等数论自我探索 » 用初等数论探索哥德巴赫猜想 » (二) 给定正整数 \\(x\\) 若整除互质数对\\( a, b \\)之一,则 \\(x\\) 不整除 \\(a-b\\)","id":"115","title":"(二) 给定正整数 \\(x\\) 若整除互质数对\\( a, b \\)之一,则 \\(x\\) 不整除 \\(a-b\\)"},"116":{"body":"分类讨论: 若n为素数, 显然 \\( 2n-n = n \\) 亦为素数, 哥德巴赫猜想成立. [1] 若n为合数, 则 \\( n \\ge 4 \\). 由 引理(一) , 将集合P以能否整除n划分为以下子集: \\( S = \\lbrace s \\mid s|n \\rbrace, T = \\lbrace t \\mid t \\nmid n \\rbrace\\) 容易发现以下结论: \\( \\forall s \\in S, \\because s|n, s|s, \\therefore s|2n-s \\), 即若一个素数是n的素因子, 那么对应的整数 \\( 2n-s \\) 为合数. 故 符合哥德巴赫猜想的数对 \\( p \\)与 \\( 2n-p \\)必然满足 \\( p \\nmid n \\)(封面结论) 到这里, 我们可以得到一个哥德巴赫猜想的等价命题: 对给定合数\\(n\\), 及小于\\(n\\)且不整除\\(n\\)的素数集合\\( T = \\lbrace t \\mid Prime(t) \\land t \\lt n \\land t \\nmid n \\rbrace \\), 在集合\\(T\\)对应的整数集\\(2n-T = \\lbrace 2n - t \\mid Prime(t) \\land t \\lt n \\land t \\nmid n \\rbrace \\)中是否存在素数","breadcrumbs":"初等数论自我探索 » 用初等数论探索哥德巴赫猜想 » 探索哥德巴赫猜想","id":"116","title":"探索哥德巴赫猜想"},"117":{"body":"","breadcrumbs":"初等数论自我探索 » 用初等数论探索哥德巴赫猜想 » 进一步探究","id":"117","title":"进一步探究"},"118":{"body":"\\[ \\label{3} \\tag{3} \\forall s \\in S, t \\in T, s \\nmid 2n-t \\] 而由 引理(二) 可得: 对于任意前述s, t, 有: \\( Prime(s), Prime(t) \\Rightarrow s \\nmid t \\) \\( s|n \\Rightarrow s|2n \\) 故: \\( \\forall s \\in S, t \\in T, s \\nmid 2n-t \\) 也就是, 至少集合 \\( 2n-T \\) 的元素不会被 \\( S \\) 中的元素整除, 命题 \\( \\ref{3} \\) 证毕。","breadcrumbs":"初等数论自我探索 » 用初等数论探索哥德巴赫猜想 » 推论一","id":"118","title":"推论一"},"119":{"body":"对于\\(T\\)的子集 \\( T_{\\gt {n \\over 2}} = T_1 = \\lbrace t| t \\in T, t \\gt {n \\over 2} \\rbrace \\), 具有以下性质: \\[ \\label{4} \\tag{4} \\forall t_1, t_2 \\in T_1, t_1 \\nmid 2n-t_2 \\] 证明如下: 假设 \\( \\exists t_1, t_2, t_1 \\mid 2n-t_2 \\), 即 \\( \\exists m \\in Z^+, mt_1 = 2n - t_2 \\). \\( \\because t_1, t_2为奇数, 2n为偶数 \\) \\( \\therefore m为奇数. \\) 分类讨论: 当 \\( m = 1 \\) 时, t_1 + t_2 = 2n. \\( \\because t_1 \\neq t_2, t_1 \\lt n, t_2 \\lt n, \\therefore t_1 + t_2 \\lt 2n \\), 矛盾! 当 \\( m \\ge 3 \\)时: \\( \\because t_1 \\gt {n \\over 2}, t_2 \\gt {n \\over 2}, \\therefore mt_1 + t_2 \\gt { mn \\over 2 } + { n \\over 2 } \\ge { 3n \\over 2 } + { n \\over 2 } = 2n \\) , 矛盾! 故假设不成立, 命题 \\( \\ref{4} \\) 证毕. 如无特殊说明,本文中所有集合均为正整数集的子集 在本文中暂且抛去哥德巴赫猜想中对分解出的两个素数不能相等的要求。","breadcrumbs":"初等数论自我探索 » 用初等数论探索哥德巴赫猜想 » 推论二","id":"119","title":"推论二"},"12":{"body":"demo |-- Cargo.toml |-- src |-- lib.rs |-- assets |-- demo.html # Cargo.toml\n[package]\nname = \"demo\"\nauthors = [\"huangjj27 \"]\nversion = \"0.1.0\"\nedition = \"2021\" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [lib]\ncrate-type = [\"cdylib\", \"rlib\"] [dependencies]\nwasm-bindgen = \"0.2.63\" // lib.rs\nuse wasm_bindgen::prelude::*; #[wasm_bindgen]\npub fn add(a: i32, b: i32) -> i32 { a + b\n} \n\n\n just a demo \n\n \n\n 项目在编译的时候还需要 wasm-pack ,用来生成胶水 JavaScript demo/pkg/demo.js 和 wasm 文件 demo/pkg/demo_bg.wasm: wasm-pack build --target=web","breadcrumbs":"尝试在单 HTML 文件中嵌入 WASM 模块的错误操作 » demo 项目架构","id":"12","title":"demo 项目架构"},"120":{"body":"77u/KCs4NikxODgtMTk0OC0xMjYyDQo= · huangjj.27@qq.com · 349373001 · huangjj27 · huangjj27 下载简历pdf · tech-blog: https://huangjj27.github.io · 微信技术公众号: 坏姐姐日常入门Rust","breadcrumbs":"关于我 » 黄俊杰","id":"120","title":"黄俊杰"},"121":{"body":"2013.9 -- 2017.7 中山大学 数据科学与计算机学院(原软件学院) 软件工程 工学学士","breadcrumbs":"关于我 » 教育背景","id":"121","title":"教育背景"},"122":{"body":"","breadcrumbs":"关于我 » 项目经历","id":"122","title":"项目经历"},"123":{"body":"测试工程师 : 业务测试、自动化测试、性能测试 负责数字化营业厅项目下的叫号系统与数据赋能看板项目的功能测试 针对热点性能的接口与流程进行性能测试 辅助运营人员排查与定位营业员反馈的生产问题 Bonus: 编写接口自动化测试用例(Postman),并设定生产环境的每日自动巡检(企业微信 bot) 使用 goose 、 locus 框架编写性能测试用例 制定性能测试需求评估、性能测试报告规范 review 项目代码,并通过项目代码补充对 DES、AES、HTTP设计规范的了解","breadcrumbs":"关于我 » 数字化营业厅 2021.09 - 至今","id":"123","title":"数字化营业厅 2021.09 - 至今"},"124":{"body":"大数据开发工程师: 大数据 EDI 开发 负责子项目的架构优化、详细设计及部分代码实现 使得原本执行需要 40 小时的作业降至平均完成时间 6 小时 通过 GitLab 管理项目源代码,进行问题追踪、代码评审、自动化流水构建、自动化测试执行 负责子项目程序优化,进行 excel 配置自动化转化为数据库工具的开发 负责子项目部分功能的测试,利用了 Python 工具自动化执行测试用例 负责子项目中关键词词频分析相关部分程序的维护","breadcrumbs":"关于我 » 银行业客户经理智能推荐与客户反馈收集项目 2020.11 - 2021.09","id":"124","title":"银行业客户经理智能推荐与客户反馈收集项目 2020.11 - 2021.09"},"125":{"body":"大数据开发工程师: 大数据 EDI 开发 自发地将银行客户用于 Hive QL 静态扫描规则的工具 CLI 化改造(基于 Python) 与上下游沟通,将该工具部署至 CI 平台 该工具成功阻止多次高风险代码提交 对相关员工讲演培训","breadcrumbs":"关于我 » HiveQL 静态代码扫描检查工具 2019.4","id":"125","title":"HiveQL 静态代码扫描检查工具 2019.4"},"126":{"body":"大数据开发工程师: 大数据 EDI 开发 基于银行客户内 EDI 框架(基于 Hadoop 与 Hive)进项业务项目开发 基于 Hive 特性与调度流程优化提高已有项目代码效率 组织相关开发经验分享 (下列项目均为业余项目/开源贡献项目)","breadcrumbs":"关于我 » 客户个人金融业务管理平台 2018.8 -- 2019.4","id":"126","title":"客户个人金融业务管理平台 2018.8 -- 2019.4"},"127":{"body":"Asynchronous Programming in Rust 一书翻译 该书详细地介绍了在 Rust 中异步编程的基础设施 Future trait、Waker类型,为了使 Future 正常工作的 Pin 智能指针与 Unpin trait,以及方便开发而引用的 async/await 语法糖 该书亦给出了示例构建一个简单的执行器,以及实现一个简单的利用异步优化性能的简单 HTTP 服务器","breadcrumbs":"关于我 » 《Rust 中的异步编程》","id":"127","title":"《Rust 中的异步编程》"},"128":{"body":"在 std 环境下使用比较广泛的 logger 为该库 实现了基础的 wasm32-unknown-unknown 目标的支持 , 让该库支持浏览器环境 因为内部结构实现的原因(formatter 格式化后记录丢失了 log::Level 信息,writter 直接 使用前述记录写入日志),暂时未实现在浏览器环境中的 log 分级。","breadcrumbs":"关于我 » env_logger","id":"128","title":"env_logger"},"129":{"body":"参考了 Python 程序实现 补足了单元测试用例、集成测试用例","breadcrumbs":"关于我 » TLSSigAPI - 使用 Rust 重写 Tencent Login Service Signature API","id":"129","title":"TLSSigAPI - 使用 Rust 重写 Tencent Login Service Signature API"},"13":{"body":"当我们打开 demo.html 并且尝试修改 a 和 b 的值时,我们会从控制台遇到了如下报错: 13:33:19.672 Uncaught ReferenceError: add is not defined js_add file:///demo/assets/demo.html:23 oninput file:///demo/assets/demo.html:1\n2 demo.html:22:13 这里的 add 函数就是我们从 WASM 模块中加载的,来自 rust 实现的 add 函数。此时,如果我们在 run 下属下方,直接调用 add 则是可以执行成功的: 执行结果: 14:02:58.360 add函数已加载,add(1, 2) = 3 demo.html:13:21\n14:03:11.895 Uncaught ReferenceError: add is not defined js_add file:///demo/assets/demo.html:23 oninput file:///demo/assets/demo.html:1\n2 demo.html:23:13 出现以上现象的原因是,type=\"module\" 限制了 indexscript 内部项目的作用域只能在该 \n\n \n\n","breadcrumbs":"尝试在单 HTML 文件中嵌入 WASM 模块的错误操作 » 薛定谔的 JavaScript 函数","id":"13","title":"薛定谔的 JavaScript 函数"},"130":{"body":"后端开发/web 开发 熟悉 Rust-lang,熟悉生命周期约束、所有权系统并对其进行分析 熟悉面向对象编程的概念以及 SOLID原则 熟悉基本的算法与数据结构 了解 RESTful API设计 版本管理 熟练使用 git/github/gitlab进行代码版本管理 具有良好的版本管理意识, 熟悉 语义化版本 规则 软件测试 -- 有功能测试、单元测试、集成测试、接口测试经验,也熟悉使用 Rust 测试套件 外语 -- 英语(CET6)","breadcrumbs":"关于我 » 技能","id":"130","title":"技能"},"14":{"body":"我们来分析一下 wasm-pack 生成的 demo.js: // demo.js let wasm; /**\n* @param {number} a\n* @param {number} b\n* @returns {number}\n*/\nexport function add(a, b) { const ret = wasm.add(a, b); return ret;\n} async function load(module, imports) { if (typeof Response === 'function' && module instanceof Response) { if (typeof WebAssembly.instantiateStreaming === 'function') { try { return await WebAssembly.instantiateStreaming(module, imports); } catch (e) { if (module.headers.get('Content-Type') != 'application/wasm') { console.warn(\"`WebAssembly.instantiateStreaming` failed because your server does not serve wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\\n\", e); } else { throw e; } } } const bytes = await module.arrayBuffer(); return await WebAssembly.instantiate(bytes, imports); } else { const instance = await WebAssembly.instantiate(module, imports); if (instance instanceof WebAssembly.Instance) { return { instance, module }; } else { return instance; } }\n} async function init(input) { if (typeof input === 'undefined') { input = new URL('demo_bg.wasm', import.meta.url); } const imports = {}; if (typeof input === 'string' || (typeof Request === 'function' && input instanceof Request) || (typeof URL === 'function' && input instanceof URL)) { input = fetch(input); } const { instance, module } = await load(await input, imports); wasm = instance.exports; init.__wbindgen_wasm_module = module; return wasm;\n} export default init; 我们看到,demo.js:175 对来待加载的 module 做了判断:如果不是从响应获取的数据,则直接视作 wasm bytes 来进行加载。于是我们可以对生成的 demo_bg.wasm 文件通过 base64 转码,嵌入到 HTML 文件中,然后转换成 Uint8Array 传递给 demo.js:init 函数进行加载: 完成以上步骤,我们就得到了一个带有 WASM 功能的 standalone html 文件啦~。","breadcrumbs":"尝试在单 HTML 文件中嵌入 WASM 模块的错误操作 » 然而我还是想包容你的,我的 WASM","id":"14","title":"然而我还是想包容你的,我的 WASM"},"15":{"body":"免责声明:禁止任何个人或团体使用本文研究成果用于实施任何违反中华人民共和国法律法规的活动 如有违反,均与本文作者无关 在我们通常遇到的抽奖场景,于年会时将所有人的编号都放到箱子里面抽奖,然后每次抽出中奖者 决定奖项。而在这过程中,因为先抽中者已经确定了奖项,然后不能够参与后续的奖项的抽奖;而后 续参与抽奖的人员则其实会以越来越低的概率参与抽奖: 例:在上述场景中共有 \\( n \\) 人参与抽取 \\( m ( \\lt n) \\) 个奖项, 抽取第一个奖项概率为: \\( { m \\over n } \\) 那么如果第一个奖项被抽走并 揭露了 ,剩下 \\( n - 1 \\) 人参与 \\( m - 1 \\) 个奖项,抽中的概率 为 \\( m - 1 \\over n - 1 \\)。 那么 \\( m \\lt n \\Rightarrow -m \\gt -n \\Rightarrow mn - m \\gt nm - n \\Rightarrow m(n-1) \\gt n(m - 1) \\Rightarrow { m \\over n } \\gt { m - 1 \\over n - 1 }\\), 即如果是后续参与抽奖 并且前面的奖项被拿走了 ,后面抽到奖项的概率会更低, 同时也会失去参与部分奖项的机会 。 因此,在人数 \\( n \\) 大于奖项数 \\( m \\) 的时候,我们通过以越来越低的概率干涉前面 已经“取得”奖项的结果,来保证先参与抽奖的人中奖的概率随着人数的增多中奖的概率也变低, 最后保证每个人中奖的概率为 \\( m \\over n \\)。但是在实际场景中,\\( m \\) 个奖项可能 不仅相同(如划分了一二三等奖),因此对于蓄水池算法的改进提出了新的要求: 将所有的奖项视为各不相同的位置,不论人数多少(当还是要保证有人来参与抽奖 \\( n \\gt 1\\) )所有人占有特定位置的概率相同 每当新来一人参与抽奖时,如果他没有中奖,可以即场告知未中 [1]","breadcrumbs":"蓄水池算法改进 - 面向抽奖场景保证等概率性 » 蓄水池算法改进 - 面向抽奖场景保证等概率性","id":"15","title":"蓄水池算法改进 - 面向抽奖场景保证等概率性"},"16":{"body":"我们分两种情况讨论: 一种是当人数不足以覆盖所有的奖项的场景( \\(n \\lt m \\) ), 另外一种是当抽奖人数远大于所有奖项加起来的数目。( \\( n \\gt m \\))。 然后我们再回来看看能不能找到一种很方便的方法桥接两种情况。 同时,我们假设 \\( m \\) 个奖项两两互不相同。","breadcrumbs":"蓄水池算法改进 - 面向抽奖场景保证等概率性 » 算法描述与等概率性证明","id":"16","title":"算法描述与等概率性证明"},"17":{"body":"因为当人数不足时,所有参与者都能抽奖,因此我们要保证每个人获得特定奖项的概率为 \\( 1 \\over m \\)。 算法描述: 记 \\( Choosen \\) 为容量为 \\( m \\) 的数组, \\( Choosen[k] (1 \\le k \\le m) \\) 表示第 k 个奖项的当前占有情况, 初始值为 \\( None \\), \\( Players \\) 为参与参与抽奖的人的序列 令 \\( i := 1 \\),当 \\( i \\le n \\) 时,做如下操作: 产生随机数 \\( r_1 (1 \\le r_1 \\le i) \\) 如果 \\( r_1 \\lt i \\),\\( Choosen[i] := Choosen[r_1] \\) \\( Choosen[r_1] := Players[i] \\) \\( i := i + 1 \\) 当 \\( i \\le m \\) 时,做如下操作: 产生随机数 \\( r_2 (1 \\le r_2 \\le i) \\) 如果 \\( r_2 \\lt i \\): \\( Choosen[i] := Choosen[r_2] \\) \\( Choosen[r_2] := None \\) \\( i := i + 1 \\) 等概率性证明 我们先证明,在填入中奖者的第 \\( k (1 \\le k \\le m) \\) 轮过程中,能够保证对于前 \\( k \\) 个奖项中的每一个奖项,每一位中奖者抽中其中第 \\( i (1 \\le i \\le k) \\) 个奖项的概率为 \\(1 \\over k \\),证明如下: 我们采用数学归纳法来证明: 奠基 :当 \\( k = 1 \\) 时,易知该中奖者一定会抽中第一个奖项,前一个奖项中只有第一个 选项,所以此时每一位中奖者抽中第 \\( k = 1 \\) 的概率为 \\( 1 = { 1 \\over 1 } = { 1 \\over k } \\); 归纳 : 假设当 \\(k = j (1 \\le j \\lt m) \\)时,每一位抽奖者抽中第 \\( i (1 \\le i \\le j) \\)的概率为 \\( 1 \\over j \\) 当 \\( k = j + 1 \\), 有: 第 \\( j + 1 \\) 位抽奖着抽中任意第 \\( i' (1 \\le i' \\le j + 1) \\) 个奖项的概率为 \\( 1 \\over { j + 1 } \\) (假设产生的随机数 \\( r_1、r_2 \\) 足够的均匀); 对于前 \\( j \\) 位抽奖者,每一位都有 \\( 1 \\over { j + 1 } \\),的概率将自己的奖项更换位第 \\( j + 1 \\)个奖项; 对于前 \\( j \\) 位抽奖者,每一位依然占有原有第 \\( i' \\) 个奖项的概率为: \\[ \\begin{equation} \\begin{aligned} P\\{前 j 位抽奖者 j + 1 轮中仍然持有 i' \\} & = P\\{前 j 位抽奖者j轮已经持有 i' \\} \\cdot P\\{第 j + 1 位抽奖者没有抽中 i' \\} \\\\ & = P\\{前 j 位抽奖者j轮已经持有 i' \\} \\cdot (1 - P\\{第 j + 1 位抽奖者抽中 i' \\}) \\\\ & = \\frac{1}{j} \\cdot (1 - \\frac{1}{j+1}) \\\\ & = \\frac{1}{j} \\cdot \\frac{j}{j+1} \\\\ & = \\frac{1}{j + 1} \\\\ & = \\frac{1}{k} \\\\ \\end{aligned} \\label{1.1} \\tag{1.1} \\end{equation} \\] 由上,可知每一轮迭代之后,前 \\( k \\) 个奖项对于已经参与的 \\( k \\)中奖者来说抽中的概率均等,为 \\( 1 \\over k \\), 故到了第 \\( n \\) 轮操作后,我们可以通过不断填充 \\( None \\)值来稀释概率,最后达到 \\( 1 \\over m \\) 的等概率性。 特殊地,当 \\( n == m \\) 时,每个抽奖者抽到特定奖项的概率也为 \\(1 \\over n \\)。","breadcrumbs":"蓄水池算法改进 - 面向抽奖场景保证等概率性 » 抽奖人数不足时( \\(n \\lt m \\) )","id":"17","title":"抽奖人数不足时( \\(n \\lt m \\) )"},"18":{"body":"类似地,当 \\(n \\gt m \\)时,对于每一个抽奖序号 \\( k \\gt m \\) 的抽奖者,我们生成随机数 \\( r_3(1 \\le r_3 \\le n) \\),并且在 \\( r_3 \\le m \\) 的时候,替换对应原本占有奖项的抽奖者;可以证明在这种情况下,能保证每个人抽到特定奖项的概率为 \\(1 \\over n \\) [2] 。","breadcrumbs":"蓄水池算法改进 - 面向抽奖场景保证等概率性 » 抽奖人数足够多时( \\(n \\gt m \\) )","id":"18","title":"抽奖人数足够多时( \\(n \\gt m \\) )"},"19":{"body":"记 \\( Choosen \\) 为容量为 \\( m \\) 的数组, \\( Choosen[k] (1 \\le k \\le m) \\) 表示第 \\( k \\) 个奖项的当前占有情况, 初始值为 \\( None \\), \\( replaced \\) 为原本已经中奖,但是被人替换的抽奖者 \\( Players \\) 为参与参与抽奖的人的序列,每次只能获取一个 \\( player \\) 记 \\( n := 0 \\)为当前参与抽奖的人数 在抽奖结束前,每次遇到一个新的 \\( player \\) 执行以下操作: \\( replaced := None \\) \\( n := n + 1 \\) 产生随机数 \\( r (1 \\le r \\le n) \\) 如果 \\( r \\le m \\): \\( replaced := Choosen[r] \\) \\( Choosen[r] := player \\) 如果 \\( r \\lt n \\) 并且 \\( n \\le m \\): \\( Choosen[n] := replaced \\) 在抽奖结束时,如果 \\( n \\lt m \\), 执行以下操作: \\( i := n \\) 当 \\( i \\lt m \\)时,重复执行以下操作: \\( i := i + 1 \\) 产生随机数 \\( r_2 (1 \\le r_2 \\le i) \\) 如果 \\( r_2 \\lt i \\): \\( Choosen[i] := Choosen[r_2] \\) \\( Choosen[r_2] := None \\)","breadcrumbs":"蓄水池算法改进 - 面向抽奖场景保证等概率性 » 整合后的算法","id":"19","title":"整合后的算法"},"2":{"body":"正如博主所说,在 有问题版本的插件代码 中,博主最初发现,tauri.conf.json 中的 package.productName 在分别使用五个汉字与六个汉字时,单例模式功能表现出不一致的行为:五个汉字的 productName 单例功能运行正常,而六个汉字的 productName 单例功能失效,于是针对该问题初步进行了测试: 测试的 productName 单例插件功能是否生效 六个汉字试试 x 随便五个字 √ 又来了六个字 x 因为这些汉字测试用例使用 UTF-8 编码,又因为是常用字,因此每个汉字对应 3 bytes,因此假设 productName 在超过 15 bytes、不超过 18 bytes 时会导致功能失效,进一步补充测试用例: 测试的 productName 单例插件功能是否生效 z12345678901234 √ z123456789012345 x 看来博主运气不错,刚好踩到了边界的测试用例。那么基本可以确定,productName 超过 15 bytes 就会导致单例功能失效。","breadcrumbs":"由 tauri 单例模式 bug “意外修复” 发现的 dangling » 所以这个 bug 的现象是怎么样的?","id":"2","title":"所以这个 bug 的现象是怎么样的?"},"20":{"body":"","breadcrumbs":"蓄水池算法改进 - 面向抽奖场景保证等概率性 » 程序实现","id":"20","title":"程序实现"},"21":{"body":"作者偏好 Rust 编程语言 ,故使用 Rust 实现。 特质(trait) Rust 中的 特质(trait) 是其用于复用行为抽象的特性,尽管比起 Java 或 C# 的接口 (Interface)更加强大,但在此文中, 熟悉 Java/C# 的读者把特质视作接口就可以了。","breadcrumbs":"蓄水池算法改进 - 面向抽奖场景保证等概率性 » Rust","id":"21","title":"Rust"},"22":{"body":"本文使用面向对象(Object-Oriented)编程范式 [3] 来进行抽象,如下所示: use rand::random; use std::fmt::Debug; trait ReservoirSampler { // 每种抽样器只会在一种总体中抽样,而总体中所有个体都属于相同类型 type Item; // 流式采样器无法知道总体数据有多少个样本,因此只逐个处理,并返回是否将样本纳入 // 样本池的结果,以及可能被替换出来的样本 fn sample(&mut self, it: Self::Item) -> (bool, Option); // 任意时候应当知道当前蓄水池的状态 fn samples(&self) -> &[Option];\n} struct Lottery

{ // 记录当前参与的总人数 total: usize, // 奖品的名称与人数 prices: Vec, // 当前的幸运儿 lucky: Vec>,\n} #[derive(Clone, Debug)]\nstruct Price { name: String, cap: usize,\n} impl

ReservoirSampler for Lottery

{ type Item = P; fn sample(&mut self, it: Self::Item) -> (bool, Option) { let lucky_cap = self.lucky.capacity(); self.total += 1; // 概率渐小的随机替换 let r = random::() % self.total + 1; let mut replaced = None; if r <= lucky_cap { replaced = self.lucky[r - 1].take(); self.lucky[r - 1] = Some(it); } if self.total <= lucky_cap && r < self.total { self.lucky[self.total - 1] = replaced.take(); } (r <= lucky_cap, replaced) } fn samples(&self) -> &[Option] { &self.lucky[..] }\n} impl Lottery

{ fn release(self) -> Result)>, &'static str> { let lucky_cap = self.lucky.capacity(); if self.lucky.len() == 0 { return Err(\"No one attended to the lottery!\"); } let mut final_lucky = self.lucky.into_iter().collect::>>(); let mut i = self.total; while i < lucky_cap { i += 1; // 概率渐小的随机替换 let r = random::() % i + 1; if r <= lucky_cap { final_lucky[i - 1] = final_lucky[r - 1].take(); } } println!(\"{:?}\", final_lucky); let mut result = Vec::with_capacity(self.prices.len()); let mut counted = 0; for p in self.prices { let mut luck = Vec::with_capacity(p.cap); for i in 0 .. p.cap { if let Some(it) = final_lucky[counted + i].take() { luck.push(it); } } result.push((p.name, luck)); counted += p.cap; } Ok(result) }\n} // 构建者模式(Builder Pattern),将所有可能的初始化行为提取到单独的构建者结构中,以保证初始化\n// 后的对象(Target)的数据可靠性。此处用以保证所有奖品都确定后才能开始抽奖\nstruct LotteryBuilder { prices: Vec,\n} impl LotteryBuilder { fn new() -> Self { LotteryBuilder { prices: Vec::new(), } } fn add_price(&mut self, name: &str, cap: usize) -> &mut Self { self.prices.push(Price { name: name.into(), cap }); self } fn build(&self) -> Lottery

{ let lucky_cap = self.prices.iter() .map(|p| p.cap) .sum::(); Lottery { total: 0, prices: self.prices.clone(), lucky: std::vec::from_elem(Option::

::None, lucky_cap), } }\n} fn main() { let v = vec![8, 1, 1, 9, 2]; let mut lottery = LotteryBuilder::new() .add_price(\"一等奖\", 1) .add_price(\"二等奖\", 1) .add_price(\"三等奖\", 5) .build::(); for it in v { lottery.sample(it); println!(\"{:?}\", lottery.samples()); } println!(\"{:?}\", lottery.release().unwrap());\n}","breadcrumbs":"蓄水池算法改进 - 面向抽奖场景保证等概率性 » 建模与实现","id":"22","title":"建模与实现"},"23":{"body":"流式处理,可以适应任意规模的参与人群 在保证每一位抽奖者都有相同的概率获得特定奖项的同时,还能保证每一个抽奖者的获得的奖项均不相同","breadcrumbs":"蓄水池算法改进 - 面向抽奖场景保证等概率性 » 优点","id":"23","title":"优点"},"24":{"body":"所有参与抽奖的人都必须 依次 经过服务器处理,因为需要获知准确的总人数来保证等概率性。 一个改进的方法是,在人数足够多的时候,将总人数用总人数的特定数量级替代(给后续参加者的 一点点小福利——但是因为总人数足够多,所以总体中奖概率还是很低),在客户端完成中奖的选定 等概率性完全依赖随机数 r 生成 。 因为奖品初始化时不需要考虑打乱顺序,因此如果在 随机这一步被技术破解,使得抽奖者可以选择自己能获取的奖项,则会破坏公平性。改进方案是, 在 release 的时候再一次对奖品顺序进行随机的打乱。 这种抽奖方式还限定了每人只能抽取一次奖品,否则会出现一个人占有多个奖项的情况。 可以参考 博主以前的博客 作者理解的面向对象 = 对象是交互的最基本单元 + 对象通过相互发送消息进行交互。而特质/接口以及对象其他公开的方法定义了对象可以向外发送/从外接收的消息。 该条件为用以减轻开奖时发通知的压力,并非核心需求,因为对参与抽奖的玩家负责的原因,我们还是需要储存每个玩家每次的抽奖情况信息","breadcrumbs":"蓄水池算法改进 - 面向抽奖场景保证等概率性 » 缺点","id":"24","title":"缺点"},"25":{"body":"目前所有抽奖者都按照相等的概率抽奖,而在一些场景下可能按照一些规则给与某些抽奖者优惠 (例如绩效越高的员工中奖概率越大),因此下一步可能考虑如何按照权重赋予每位抽奖者各自的 中奖概率。","breadcrumbs":"蓄水池算法改进 - 面向抽奖场景保证等概率性 » 下一步可能展开的工作","id":"25","title":"下一步可能展开的工作"},"26":{"body":"感谢茶壶君( @ksqsf )一语惊醒梦中人,清楚明确地表达了需求; 感谢张汉东老师 ( @ZhangHanDong )老师提点了之后可以开展研究的方向; 感谢在这次讨论中提供意见的其他 Rust 社区的朋友,谢谢你们!","breadcrumbs":"蓄水池算法改进 - 面向抽奖场景保证等概率性 » 致谢","id":"26","title":"致谢"},"27":{"body":"","breadcrumbs":"记一次面试 » 总结一次面试","id":"27","title":"总结一次面试"},"28":{"body":"","breadcrumbs":"记一次面试 » 最值得总结的三个问题","id":"28","title":"最值得总结的三个问题"},"29":{"body":"线程同步的目的在于解决多个线程访问关键资源时的竞争状态。一个数据竞争的简单例子如下: use std::thread;\nfn main() { let mut s = String::from(\"Hello\"); let thread1 = thread::spawn(|| { println!(\"{}\", s); }); let thread2 = thread::spawn(|| { s.push_str(\"World!\"); println!(\"{}\", s); }); thread1.join(); thread2.join();\n} 上文的代码中 thread1 试图打印 s, 预期得到输出 Hello, 但是 thread2 却改变了 s 的内容, 那么 thread1 最终打印内容将取决于两个线程哪个先完成: 如果 thread1 先完成了,那么 将打印 Hello; 如果 thread2 先完成了,那么将打印 HelloWorld!。 实际上得益于 Rust 的所有权系统与生命周期(lifetime)检查,上述示例并不能编译——子线程可能 会在主程序结束后继续运行,导致子线程捕获的 s 的引用失效;另外 thread2 直接修改了 s, 换言之只会允许 thread2 独占地持有 s 的可变引用(&mut s),而不允许其他线程持有 s 的任何引用。 在 Rust 编程中,主要有以下线程同步的方法: 互斥锁(Mutex) 我们可以使用互斥锁 Mutex 来控制只能有单独一个线程读取/修改 对象。通常实践是在外面加上原子引用计数 Arc 变成 Arc>,来减少 Mutex 拷贝的开销。对于多读少写的场景,可以用 RwLock 提高并发。 条件变量(CondVar) 条件变量用于“阻塞”线程并使得线程在等待事件时不需要消耗 CPU 时间。通常会与放进互斥锁 布尔型的预言值(状态值)关联使用,在状态值发生变化时通知条件变量。 屏障(Barrier) 屏障用于在某个需要若干线程 都完成 前置操作后再开始计算的操作之前,让所有所需线程 的状态都达到能开始进行计算的状态。","breadcrumbs":"记一次面试 » 线程同步有哪些方法?如何用这些方法实现一个 RwLock?","id":"29","title":"线程同步有哪些方法?如何用这些方法实现一个 RwLock?"},"3":{"body":"在最初的讨论过程中,因为我们没有仔细留意插件仓库使用的 encode_wide 是自行做过封装的,因此我们一开始根据 以下代码 进行分析: pub fn init(f: Box>) -> TauriPlugin { plugin::Builder::new(\"single-instance\") .setup(|app| { let app_name = &app.package_info().name; let class_name = format!(\"{}-single-instance-class\", app_name); let window_name = format!(\"{}-single-instance-window\", app_name); let hmutex = unsafe { CreateMutexW( std::ptr::null(), true.into(), encode_wide(\"tauri-plugin-single-instance-mutex\").as_ptr(), ) }; if unsafe { GetLastError() } == ERROR_ALREADY_EXISTS { unsafe { let hwnd = FindWindowW( encode_wide(&class_name).as_ptr(), encode_wide(&window_name).as_ptr(), ); // omitted } // omitted } // omitted }) .on_event(|app, event| { // omitted }) .build()\n} 有 Windows 编程经验的 @PotatoTooLarge 指出,encode_wide(来自 std 的 Window扩展 )并不会补充 \\0 结束符: Re-encodes an OsStr as a wide character sequence, i.e., potentially ill-formed UTF-16. This is lossless: calling OsStringExt::from_wide and then encode_wide on the result will yield the original code units. Note that the encoding does not add a final null terminator. 于是博主听取建议,把所有会传递到 encode_wide 函数的字符串都添加了 \\0,形成了 一版修复 : pub fn init(f: Box>) -> TauriPlugin { plugin::Builder::new(\"single-instance\") .setup(|app| { let app_name = &app.package_info().name; // let class_name = format!(\"{}-single-instance-class\", app_name); let class_name = format!(\"{}-single-instance-class\\0\", app_name); // let window_name = format!(\"{}-single-instance-window\", app_name); let window_name = format!(\"{}-single-instance-window\\0\", app_name); let hmutex = unsafe { CreateMutexW( std::ptr::null(), true.into(), // encode_wide(\"tauri-plugin-single-instance-mutex\").as_ptr(), encode_wide(\"tauri-plugin-single-instance-mutex\\0\").as_ptr(), ) }; if unsafe { GetLastError() } == ERROR_ALREADY_EXISTS { unsafe { let hwnd = FindWindowW( encode_wide(&class_name).as_ptr(), encode_wide(&window_name).as_ptr(), ); // omitted } // omitted } // omitted }) .on_event(|app, event| { // omitted }) .build()\n} 然后使用修复前会引起单例功能失效的 z123456789012345 作为测试用例,验证单例功能可用了,证明该修改可以修复单例功能失效的问题。","breadcrumbs":"由 tauri 单例模式 bug “意外修复” 发现的 dangling » PotatoTooLarge: 传递给 Win32 API 的字符串要用 C string 风格的 \\0 结束","id":"3","title":"PotatoTooLarge: 传递给 Win32 API 的字符串要用 C string 风格的 \\0 结束"},"30":{"body":"这道问题最后我也并没有理解,“生命周期标注无法修正的问题”,字面意思是,即使我们按照我们 期望的程序语义来修正了生命周期标注,这个程序仍然不能通过编译,或者再运行时仍然不能得到 期望结果。按此描述,一个可能的例子是,我们尝试从一个较短的引用返回一个较长的引用: fn longhten<'a>(s_ref: &'a str) -> &'static str { s_ref\n} fn main() { let s = String::from(\"hello\"); let static_ref = longhten(&s); println!(\"{}\", static_ref);\n}","breadcrumbs":"记一次面试 » 有什么问题是生存期标注无法修正的?请给出一个例子","id":"30","title":"有什么问题是生存期标注无法修正的?请给出一个例子"},"31":{"body":"Reactor 作为反应器,上面同时挂载了成千上万个待唤醒的事件,这里使用了mio统一封装了操作系统的多路复用API。 在Linux中使用的是Epoll [1] ,在Mac中使用的则是Kqueue [2] loop { // 轮询事件是否超时 poll.poll(&events, timeout); for event in events.iter() { if (event.is_readable()) { for waker in event.readers.wakers { waker.wake(); } } if (event.is_writeable()) { for waker in event.writers.wakers { waker.wake(); } } }\n}","breadcrumbs":"记一次面试 » Waker 如何被唤醒? Reactor要怎么实现?","id":"31","title":"Waker 如何被唤醒? Reactor要怎么实现?"},"32":{"body":"","breadcrumbs":"记一次面试 » 一面 -- 手写代码","id":"32","title":"一面 -- 手写代码"},"33":{"body":"# use std::cmp::Ordering;\n#\n/// 给出一个从小到大排列的数组,请实现一个函数,用二分法把指定数组 x 的位置找出来。若 x\n/// 不存在,则返回 -1. 若 x 存在多个,请返回 x 在数组中第一次出现的位置\nfn find(arr: Vec, x: i32) -> i32 { let (mut left, mut right) = (0, arr.len() - 1); loop { if left > right { return -1; } let mut mid = (left + right) / 2; match arr[mid].cmp(&x) { // 记得要排除已经命中的元素! Ordering::Less => left = mid + 1, Ordering::Greater => right = mid - 1, Ordering::Equal => { while mid >= 1 && arr[mid - 1] == x { mid -= 1; } return mid as i32; }, } }\n} # #[cfg(test)]\n# mod test {\n# use super::*;\n#\n# #[test]\n# fn should_return_minus1() {\n# let arr = vec![1, 3, 5];\n# let x = 2;\n#\n# assert_eq!(find(arr, x), -1);\n# }\n#\n# #[test]\n# fn should_return_mid() {\n# let arr = vec![1, 3, 5, 7, 9, 10, 10];\n# let x = 7;\n#\n# assert_eq!(find(arr, x), 3);\n# }\n#\n# #[test]\n# fn should_return_first() {\n# let arr = vec![1, 3, 5, 7, 9, 10, 10];\n# let x = 10;\n#\n# assert_eq!(find(arr, x), 5);\n# }\n# } Q: 请分析该函数的算法复杂度? A: 时间复杂度 \\( O(\\log n) \\),最坏情况下的事件复杂度是 \\( O(n) \\) Q: 请优化这个算法? A:一个优化方法是 插值查找法 ,利用如下公式自动根据查找到的元素与目标的距离来修正下一次查找 的区间范围,提高查找速度: \\[ mid = left + { key - arr[left] \\over arr[right] - key } (right - left) \\]","breadcrumbs":"记一次面试 » 1. 实现一个二分查找函数","id":"33","title":"1. 实现一个二分查找函数"},"34":{"body":"请反转二叉树。如给出以下二叉树: 1 / \\ 2 3 / \\ / \\\n4 5 6 7 反转为: 1 / \\ 3 2 / \\ / \\\n7 6 5 4 递归解法: # use std::convert::Into;\n#\nstruct Node { val: i32, left: NodeLink, right: NodeLink,\n} type NodeLink = Option>; # fn construct_tree() -> Box {\n# let l3left1 = Node {\n# val: 4,\n# left: None,\n# right: None,\n# };\n#\n# let l3right1 = Node {\n# val: 5,\n# left: None,\n# right: None,\n# };\n#\n# let l3left2 = Node {\n# val: 6,\n# left: None,\n# right: None,\n# };\n#\n# let l3right2 = Node {\n# val: 7,\n# left: None,\n# right: None,\n# };\n#\n# let l2left = Node {\n# val: 2,\n# left: Some(Box::new(l3left1)),\n# right: Some(Box::new(l3right1)),\n# };\n#\n#\n# let l2right = Node {\n# val: 3,\n# left: Some(Box::new(l3left2)),\n# right: Some(Box::new(l3right2)),\n# };\n#\n# Box::new(Node {\n# val: 1,\n# left: Some(Box::new(l2left)),\n# right: Some(Box::new(l2right)),\n# })\n# }\n#\n# fn construct_mirror() -> Box {\n# let l3left1 = Node {\n# val: 7,\n# left: None,\n# right: None,\n# };\n#\n# let l3right1 = Node {\n# val: 6,\n# left: None,\n# right: None,\n# };\n#\n# let l3left2 = Node {\n# val: 5,\n# left: None,\n# right: None,\n# };\n#\n# let l3right2 = Node {\n# val: 4,\n# left: None,\n# right: None,\n# };\n#\n# let l2left = Node {\n# val: 3,\n# left: Some(Box::new(l3left1)),\n# right: Some(Box::new(l3right1)),\n# };\n#\n#\n# let l2right = Node {\n# val: 2,\n# left: Some(Box::new(l3left2)),\n# right: Some(Box::new(l3right2)),\n# };\n#\n# Box::new(Node {\n# val: 1,\n# left: Some(Box::new(l2left)),\n# right: Some(Box::new(l2right)),\n# })\n# }\n#\n# impl Into> for Box {\n# fn into(mut self) -> Vec {\n# let v_left: Vec;\n# let v_right: Vec;\n# v_left = if let Some(node) = self.left.take() {\n# node.into()\n# } else {\n# Vec::new()\n# };\n#\n# v_right = if let Some(node) = self.right.take() {\n# node.into()\n# } else {\n# Vec::new()\n# };\n#\n# let mut v = Vec::new();\n# v.push(self.val);\n# v.extend(v_left.into_iter());\n# v.extend(v_right.into_iter());\n#\n# v\n# }\n# }\n#\nfn mirror(root: &mut Node) { let (mut tmp_left, mut tmp_right) = (NodeLink::None, NodeLink::None); if let Some(mut node) = root.left.take() { mirror(&mut node); tmp_left = Some(node); } if let Some(mut node) = root.right.take() { mirror(&mut node); tmp_right = Some(node); } root.left = tmp_right; root.right = tmp_left;\n} # #[cfg(test)]\n# mod test {\n# use super::*;\n#\n# #[test]\n# fn should_mirrored() {\n# let mut tree = construct_tree();\n# let expect = construct_mirror();\n#\n# let mirror = mirror(&mut tree);\n# assert_eq!( as Into>>::into(tree), as Into>>::into(expect));\n# }\n# } Q: 请优化这个算法? A:如果不用递归(因为递归会加深调用栈),可以使用 广度优先搜索算法 来自根向叶 逐层反转左右子节点的指针,并将子节点的指针放入到队列中待进行处理。 线程同步 -- 百度百科 Rust异步浅谈 -- leaxoy","breadcrumbs":"记一次面试 » 2. 镜像二叉树","id":"34","title":"2. 镜像二叉树"},"35":{"body":"原文: https://mbuffett.com/posts/bevy-snake-tutorial/#0.3 Bevy 最近普及开来了,但是相关学习资料还是很少。这篇文章尝试提供 Bevy 官方书(The Bevy book)的下一步学习。最后产品看起来像这样: 这大约是 300 行 Rust 代码;也需要花点时间深入。如果你想快进到成品代码,请点 这里 。每一个小节开头都有一份代码差异,这应该会在你不是很清晰哪里需要插入代码的时候更加清晰一点。","breadcrumbs":"用 Bevy 游戏引擎编写贪吃蛇(译) » 用 Bevy 游戏引擎编写贪吃蛇(译)","id":"35","title":"用 Bevy 游戏引擎编写贪吃蛇(译)"},"36":{"body":"点击查看差异 我们现在像 Bevy 官方书那样开始,整一个啥都不干的应用。运行 cargo new bevy-snake, 然后把以下代码放到你的 main.rs : use bevy::prelude::*; fn main() { App::build().run();\n} 我们还需要在 Cargo.toml 将 Bevy 作为依赖添加,因为我(原文作者,下同)知道这个教程之后要干嘛,我们现在也提前添加 rand库吧。 // ... [dependencies]\nbevy = \"0.3.0\"\nrand = \"0.7.3\"","breadcrumbs":"用 Bevy 游戏引擎编写贪吃蛇(译) » 新的空的 Bevy 应用","id":"36","title":"新的空的 Bevy 应用"},"37":{"body":"点击查看差异 我们现在要创建一个2D游戏,需要很多不同的系统;用来创建窗口的,用来做渲染循环的,用来处理输出的,用来处理精灵(sprites)的,等等。幸运的是,Bevy的默认插件给了我们以上所有选项: fn main() { App::build().add_plugins(DefaultPlugins).run();\n} 然而 Bevy 的默认插件不包括摄像机(camera),所以我们来插入一个 2D 摄像机,只要我们创建我们第一个系统就可以设置了: fn setup(mut commands: Commands) { commands.spawn(Camera2dComponents::default());\n} Cammands 通常用来排列命令,来更改游戏世界与资源。在这里,我们创建一个带有 2D 摄像机组件的实体。为Bevy的魔法做点准备吧: App::build() .add_startup_system(setup.system()) // <-- .add_plugins(DefaultPlugins) .run(); 我们需要做的只是在我们的函数是调用 .system(),然后 Bevy 会神奇地在启动地时候调用 commands 参数。再运行一次 app, 你应该能看到一个像这样的空窗口:","breadcrumbs":"用 Bevy 游戏引擎编写贪吃蛇(译) » 创建窗口","id":"37","title":"创建窗口"},"38":{"body":"点击查看差异 我们来写个蛇头放在窗口上吧。我们先定义几个结构体: struct SnakeHead;\nstruct Materials { head_material: Handle,\n} SnakeHead 仅仅是一个空结构体,我们会把它当作一个组件来使用,它就是像某种标签,我们会放到一个实体上,之后我们能通过查询带有 SnakeHead 组件的实体来找到这个实体。像这样的空结构体在 Bevy 中是一种常见的模式,组件经常不需要他们自己的任何状态。 Materials 以后会变成一种资源,用来存储我们给蛇头使用的材质,也会用来存储蛇身和食物的材质。 head_material 句柄应该在游戏设置的时候就应该创建好,所以我们接下来要做的是,修改我们的 setup 函数: fn setup(mut commands: Commands, mut materials: ResMut>) { commands.spawn(Camera2dComponents::default()); commands.insert_resource(Materials { head_material: materials.add(Color::rgb(0.7, 0.7, 0.7).into()), });\n} 注意: Bevy要求在注册系统时按照特定的顺序。命令(Commands) -> 资源(Resources) -> 组件(Components)/查询(Queries)。如果你在弄乱一个系统之后获得一个神秘的编译时错误,请检查你的顺序。 materials.add 会返回 Handle。我们创建了使用这个新建 handle 的 Materials 结构体。之后,我们尝试访问类型为 Materials 的资源, Bevy会找到我们这个结构体。现在我们来在新的系统里创建我们的蛇头实体,然后你会看到我们如何使用前述资源的: fn game_setup(mut commands: Commands, materials: Res) { commands .spawn(SpriteComponents { material: materials.head_material.clone(), sprite: Sprite::new(Vec2::new(10.0, 10.0)), ..Default::default() }) .with(SnakeHead);\n} 现在我们有了新的系统,它会寻找类型为 Materials 的资源。它也会创建(spawn)一个新实体,带有 SpriteComponents 和 SnakeHead 组件。为了创建 SpriteComponents, 我们将我们之间创建的颜色的 handle 传入,并且给精灵 10x10 的大小。我们将这个系统添加到我们 app 的构建器: .add_startup_system(setup.system())\n.add_startup_stage(\"game_setup\") // <--\n.add_startup_system_to_stage(\"game_setup\", game_setup.system()) // <-- 我们需要一个新的场景而不是再一次调用 add_startup_system 的原因是,我们需要使用在 setup 函数中插入的资源。这次运行后,你应该在屏幕中央看到蛇头: 好了,可能我们叫它“蛇头”有点过了,你可以看到一个 10x10 的白色精灵。","breadcrumbs":"用 Bevy 游戏引擎编写贪吃蛇(译) » 开始编写一条蛇","id":"38","title":"开始编写一条蛇"},"39":{"body":"点击查看差异 如果小蛇不运动,那么游戏很无趣,所以我们先让蛇头动起来。我们之后再担心输入,现在我们的目标是让蛇头移动。所以我们来创建一个系统来移动所有的蛇头: fn snake_movement(mut head_positions: Query<(&SnakeHead, &mut Transform)>) { for (_head, mut transform) in head_positions.iter_mut() { *transform.translation.y_mut() += 2.; }\n} 这里有个新概念, Query 类型。我们用它来迭代所有拥有 SnakeHead 组件以及 Transform 组件的实体。我们不需要担心实际上如何创建查询类型, bevy 会帮我们创建好并用它调用我们的函数,算是 ECS 魔法的一部分。所以我们来加上这个系统, 然后看看会发生些什么: .add_startup_system_to_stage(\"game_setup\", game_setup.system())\n.add_system(snake_movement.system()) // <--\n.add_plugins(DefaultPlugins) 这是我们看到的,一头蛇移出了屏幕: 你可能再思考 Transform 组件。当我们生成 SnakeHead 时,我们并没有给它 Transform,所以我们怎么就能找到一个同事拥有 SnakeHead 和 Transform 组件的实体呢?实际上 SpriteComponents 是一捆组件。就 SpriteComponents 来说,它包含了 Transform 组件,以及一堆其他组件(如 Sprite, Mesh, Draw, Rotation, Sale)。","breadcrumbs":"用 Bevy 游戏引擎编写贪吃蛇(译) » 移动小蛇","id":"39","title":"移动小蛇"},"4":{"body":"在博主提交了修复后,插件仓库作者提醒,前文所述的代码使用的 encode_wide 是 封装拼接了 \\0 后再传递参数的: pub fn encode_wide(string: impl AsRef) -> Vec { std::os::windows::prelude::OsStrExt::encode_wide(string.as_ref()) .chain(std::iter::once(0)) .collect()\n} 这意味着,并不是 \\0 导致问题的失效,因为该函数在 windows 环境下执行是能够补足 \\0 的: fn encode_wide(string: impl AsRef) -> Vec { std::os::windows::prelude::OsStrExt::encode_wide(string.as_ref()) .chain(std::iter::once(0)) .collect()\n} fn main() { let product_name = \"z123456789012345\"; // output: [122, 49, 50, 51, 52, 53, 54, 55, 56, 57, 48, 49, 50, 51, 52, 53, 0] // ^ // null concated here so it's null-terminated --| println!(\"{:?}\", encode_wide(product_name));\n}","breadcrumbs":"由 tauri 单例模式 bug “意外修复” 发现的 dangling » 但它并不是真的修复","id":"4","title":"但它并不是真的修复"},"40":{"body":"我们来修改我们小蛇的移动系统,使得我们可以控制小蛇: fn snake_movement( keyboard_input: Res>, mut head_positions: Query>,\n) { for mut transform in head_positions.iter_mut() { if keyboard_input.pressed(KeyCode::Left) { *transform.translation.x_mut() -= 2.; } if keyboard_input.pressed(KeyCode::Right) { *transform.translation.x_mut() += 2.; } if keyboard_input.pressed(KeyCode::Down) { *transform.translation.y_mut() -= 2.; } if keyboard_input.pressed(KeyCode::Up) { *transform.translation.y_mut() += 2.; } }\n} 留意到我们的查询 Query<(&SnakeHead, &mut Transform)> 改为了 Query>,其实当前版本没有必要更改,旧的查询依然能很好地工作。我想,第一个系统的类型签名可能简单些,但是现在我们用正确的方式编写类型。这写法更正确是因为我们其实不需要 SnakeHead 组件。所以 With 类型允许我们说,“我们需要那些有蛇头的实体,但是我不关心蛇头组件,只给我 transform 组件就好。”每个系统访问的组件越少,bevy就能并行越多的系统。例如,如果另外一个系统正在修改 SnakeHead 组件,那这个系统旧不能在用旧写法的时候并行了。 现在,我们能控制小蛇了,尽管它动起来不那么像蛇:","breadcrumbs":"用 Bevy 游戏引擎编写贪吃蛇(译) » 控制小蛇","id":"40","title":"控制小蛇"},"41":{"body":"点击查看差异 到现在我们一直在用窗口的坐标,但这种方法只能在 (0, 0) 坐标在窗口正中央,并且单位是像素的时候有效。贪吃蛇游戏通常用格子,所以如果我们把我们的贪吃蛇设置成 10x10,那我们的窗口会 真的 很小。我们让日子变得轻松些吧,我们选择用我们自己的位置和尺寸。然后,我们用系统来处理变换到窗口的坐标。 我们先定义格子为 10x10。在程序文件开头定义如下变量: const ARENA_WIDTH: u32 = 10;\nconst ARENA_HEIGHT: u32 = 10; 以及我们用于处理位置/尺寸的结构体: #[derive(Default, Copy, Clone, Eq, PartialEq, Hash)]\nstruct Position { x: i32, y: i32,\n} struct Size { width: f32, height: f32,\n}\nimpl Size { pub fn square(x: f32) -> Self { Self { width: x, height: x, } }\n} 相对直接地,有一个辅助方法来获取一个有相等长宽的 Size. Position 派生了一些很有用的 trait,所以我们不必不停地回顾这个结构体。 Size 可以仅仅包含一个浮点数,因为所有的对象最后都有相等的长度和宽度,但是我给它长度和宽度好像有点不对。我们现在把这些组件添加到我们生成的蛇头上: commands .spawn(SpriteComponents { material: materials.head_material.clone(), sprite: Sprite::new(Vec2::new(10.0, 10.0)), ..Default::default() }) .with(SnakeHead) .with(Position { x: 3, y: 3 }) // <-- .with(Size::square(0.8)); // <-- 这些组件暂时不做任何事情,我们现在就来将我们的尺寸映射到精灵的尺寸: fn size_scaling(windows: Res, mut q: Query<(&Size, &mut Sprite)>) { let window = windows.get_primary().unwrap(); for (sprite_size, mut sprite) in q.iter_mut() { sprite.size = Vec2::new( sprite_size.width / ARENA_WIDTH as f32 * window.width() as f32, sprite_size.height / ARENA_HEIGHT as f32 * window.height() as f32, ); }\n} 这个尺寸变换逻辑是这样的:如果某个对象有一个单位格子宽度,格子宽40,然后窗口现在 400px 宽,那么它应该有10哥宽度。下面我们做位置系统: fn position_translation(windows: Res, mut q: Query<(&Position, &mut Transform)>) { fn convert(pos: f32, bound_window: f32, bound_game: f32) -> f32 { let tile_size = bound_window / bound_game; pos / bound_game * bound_window - (bound_window / 2.) + (tile_size / 2.) } let window = windows.get_primary().unwrap(); for (pos, mut transform) in q.iter_mut() { transform.translation = Vec3::new( convert(pos.x as f32, window.width() as f32, ARENA_WIDTH as f32), convert(pos.y as f32, window.height() as f32, ARENA_HEIGHT as f32), 0.0, ); }\n} 位置变换:如果项目的 X 坐标在我们的系统中是 5,宽度是 10,并且窗口宽度是200,那么坐标应该是 5/10 * 200 - 200 / 2。我们减去一半的窗口宽度,因为我们的做消息是从左下角开始,然后替换到正中央。然后我们再加上半个格子,因为我们想要我们精灵的左下角对齐格子的左下角,而不是精灵中心对齐。 然后我们把这些系统加到我们的应用构建器上: .add_system(snake_movement.system())\n.add_system(position_translation.system()) <--\n.add_system(size_scaling.system()) <--\n.add_plugins(DefaultPlugins)\n.run(); 注意: 现在最明显的问题是小蛇被压扁了。另外一个问题是我们破环了我们的输入处理。我们先修复输入处理,然后我们得记得回来处理我们被压扁的小蛇,把它恢复原状。","breadcrumbs":"用 Bevy 游戏引擎编写贪吃蛇(译) » 码格子","id":"41","title":"码格子"},"42":{"body":"点击查看差异 我们现在配置好了格子坐标,现在我们需要更新我们的 snake_movement 系统。之前我们使用 Transform 的地方,现在替换成 Position: fn snake_movement( keyboard_input: Res>, mut head_positions: Query>,\n) { for mut pos in head_positions.iter_mut() { if keyboard_input.pressed(KeyCode::Left) { pos.x -= 1; } if keyboard_input.pressed(KeyCode::Right) { pos.x += 1; } if keyboard_input.pressed(KeyCode::Down) { pos.y -= 1; } if keyboard_input.pressed(KeyCode::Up) { pos.y += 1; } }\n}","breadcrumbs":"用 Bevy 游戏引擎编写贪吃蛇(译) » 使用我们的格子","id":"42","title":"使用我们的格子"},"43":{"body":"[1] 点击查看差异 我们上一步中的小蛇被压扁了,是因为默认的窗口尺寸并不是方形的,然而我们的格子是,所以我们每个格坐标会宽度长于高度。我们修复它最简单的方法,是在构建 app 的时候创建一个 WindowDescriptor 资源: App::build() .add_resource(WindowDescriptor { // <-- title: \"Snake!\".to_string(), // <-- width: 200, // <-- height: 200, // <-- ..Default::default() // <-- }) .add_startup_system(setup.system()) 同时,我们改一下背景颜色,插入这个 use 语句来引入 ClearColor 结构体: use bevy::render::pass::ClearColor; 然后在 app 构建器增加资源: .add_resource(ClearColor(Color::rgb(0.04, 0.04, 0.04))) 原文中这里的规格是 2000,但是 2000 的规则放 10x10 显然太大了, 这里改成 200","breadcrumbs":"用 Bevy 游戏引擎编写贪吃蛇(译) » 调整窗口大小","id":"43","title":"调整窗口大小"},"44":{"body":"现在我们的小蛇可以到处移动了,该喂点东西给它了。现在我们给 Materials 加一个 food_materials 字段: struct Materials { head_material: Handle, food_material: Handle, // <--\n} 然后把这个新材质加到我们的 setup 函数里: commands.insert_resource(Materials { head_material: materials.add(Color::rgb(0.7, 0.7, 0.7).into()), food_material: materials.add(Color::rgb(1.0, 0.0, 1.0).into()), // <--\n}); 然后我们需要 Duration 给要创建的定时器使用,而且我们还需要 random 来随机分配食物的位置。先在程序里引入这些: use rand::prelude::random;\nuse std::time::Duration; 然后我们因素两个新结构体: Food 组件让我们知道哪个实体是食物,以及一个定时制造食物的定时器: struct Food; struct FoodSpawnTimer(Timer);\nimpl Default for FoodSpawnTimer { fn default() -> Self { Self(Timer::new(Duration::from_millis(1000), true)) }\n} 至于实现 Default 的原因,会在我解释下面的系统的时候说明: fn food_spawner( mut commands: Commands, materials: Res, time: Res

{ // 记录当前参与的总人数 total: usize, // 奖品的名称与人数 prices: Vec, // 当前的幸运儿 lucky: Vec>,\n} #[derive(Clone, Debug)]\nstruct Price { name: String, cap: usize,\n} impl

ReservoirSampler for Lottery

{ type Item = P; fn sample(&mut self, it: Self::Item) -> (bool, Option) { let lucky_cap = self.lucky.capacity(); self.total += 1; // 概率渐小的随机替换 let r = random::() % self.total + 1; let mut replaced = None; if r <= lucky_cap { replaced = self.lucky[r - 1].take(); self.lucky[r - 1] = Some(it); } if self.total <= lucky_cap && r < self.total { self.lucky[self.total - 1] = replaced.take(); } (r <= lucky_cap, replaced) } fn samples(&self) -> &[Option] { &self.lucky[..] }\n} impl Lottery

{ fn release(self) -> Result)>, &'static str> { let lucky_cap = self.lucky.capacity(); if self.lucky.len() == 0 { return Err(\"No one attended to the lottery!\"); } let mut final_lucky = self.lucky.into_iter().collect::>>(); let mut i = self.total; while i < lucky_cap { i += 1; // 概率渐小的随机替换 let r = random::() % i + 1; if r <= lucky_cap { final_lucky[i - 1] = final_lucky[r - 1].take(); } } println!(\"{:?}\", final_lucky); let mut result = Vec::with_capacity(self.prices.len()); let mut counted = 0; for p in self.prices { let mut luck = Vec::with_capacity(p.cap); for i in 0 .. p.cap { if let Some(it) = final_lucky[counted + i].take() { luck.push(it); } } result.push((p.name, luck)); counted += p.cap; } Ok(result) }\n} // 构建者模式(Builder Pattern),将所有可能的初始化行为提取到单独的构建者结构中,以保证初始化\n// 后的对象(Target)的数据可靠性。此处用以保证所有奖品都确定后才能开始抽奖\nstruct LotteryBuilder { prices: Vec,\n} impl LotteryBuilder { fn new() -> Self { LotteryBuilder { prices: Vec::new(), } } fn add_price(&mut self, name: &str, cap: usize) -> &mut Self { self.prices.push(Price { name: name.into(), cap }); self } fn build(&self) -> Lottery

{ let lucky_cap = self.prices.iter() .map(|p| p.cap) .sum::(); Lottery { total: 0, prices: self.prices.clone(), lucky: std::vec::from_elem(Option::

::None, lucky_cap), } }\n} fn main() { let v = vec![8, 1, 1, 9, 2]; let mut lottery = LotteryBuilder::new() .add_price(\"一等奖\", 1) .add_price(\"二等奖\", 1) .add_price(\"三等奖\", 5) .build::(); for it in v { lottery.sample(it); println!(\"{:?}\", lottery.samples()); } println!(\"{:?}\", lottery.release().unwrap());\n}","breadcrumbs":"蓄水池算法改进 - 面向抽奖场景保证等概率性 » 建模与实现","id":"22","title":"建模与实现"},"23":{"body":"流式处理,可以适应任意规模的参与人群 在保证每一位抽奖者都有相同的概率获得特定奖项的同时,还能保证每一个抽奖者的获得的奖项均不相同","breadcrumbs":"蓄水池算法改进 - 面向抽奖场景保证等概率性 » 优点","id":"23","title":"优点"},"24":{"body":"所有参与抽奖的人都必须 依次 经过服务器处理,因为需要获知准确的总人数来保证等概率性。 一个改进的方法是,在人数足够多的时候,将总人数用总人数的特定数量级替代(给后续参加者的 一点点小福利——但是因为总人数足够多,所以总体中奖概率还是很低),在客户端完成中奖的选定 等概率性完全依赖随机数 r 生成 。 因为奖品初始化时不需要考虑打乱顺序,因此如果在 随机这一步被技术破解,使得抽奖者可以选择自己能获取的奖项,则会破坏公平性。改进方案是, 在 release 的时候再一次对奖品顺序进行随机的打乱。 这种抽奖方式还限定了每人只能抽取一次奖品,否则会出现一个人占有多个奖项的情况。 可以参考 博主以前的博客 作者理解的面向对象 = 对象是交互的最基本单元 + 对象通过相互发送消息进行交互。而特质/接口以及对象其他公开的方法定义了对象可以向外发送/从外接收的消息。 该条件为用以减轻开奖时发通知的压力,并非核心需求,因为对参与抽奖的玩家负责的原因,我们还是需要储存每个玩家每次的抽奖情况信息","breadcrumbs":"蓄水池算法改进 - 面向抽奖场景保证等概率性 » 缺点","id":"24","title":"缺点"},"25":{"body":"目前所有抽奖者都按照相等的概率抽奖,而在一些场景下可能按照一些规则给与某些抽奖者优惠 (例如绩效越高的员工中奖概率越大),因此下一步可能考虑如何按照权重赋予每位抽奖者各自的 中奖概率。","breadcrumbs":"蓄水池算法改进 - 面向抽奖场景保证等概率性 » 下一步可能展开的工作","id":"25","title":"下一步可能展开的工作"},"26":{"body":"感谢茶壶君( @ksqsf )一语惊醒梦中人,清楚明确地表达了需求; 感谢张汉东老师 ( @ZhangHanDong )老师提点了之后可以开展研究的方向; 感谢在这次讨论中提供意见的其他 Rust 社区的朋友,谢谢你们!","breadcrumbs":"蓄水池算法改进 - 面向抽奖场景保证等概率性 » 致谢","id":"26","title":"致谢"},"27":{"body":"","breadcrumbs":"记一次面试 » 总结一次面试","id":"27","title":"总结一次面试"},"28":{"body":"","breadcrumbs":"记一次面试 » 最值得总结的三个问题","id":"28","title":"最值得总结的三个问题"},"29":{"body":"线程同步的目的在于解决多个线程访问关键资源时的竞争状态。一个数据竞争的简单例子如下: use std::thread;\nfn main() { let mut s = String::from(\"Hello\"); let thread1 = thread::spawn(|| { println!(\"{}\", s); }); let thread2 = thread::spawn(|| { s.push_str(\"World!\"); println!(\"{}\", s); }); thread1.join(); thread2.join();\n} 上文的代码中 thread1 试图打印 s, 预期得到输出 Hello, 但是 thread2 却改变了 s 的内容, 那么 thread1 最终打印内容将取决于两个线程哪个先完成: 如果 thread1 先完成了,那么 将打印 Hello; 如果 thread2 先完成了,那么将打印 HelloWorld!。 实际上得益于 Rust 的所有权系统与生命周期(lifetime)检查,上述示例并不能编译——子线程可能 会在主程序结束后继续运行,导致子线程捕获的 s 的引用失效;另外 thread2 直接修改了 s, 换言之只会允许 thread2 独占地持有 s 的可变引用(&mut s),而不允许其他线程持有 s 的任何引用。 在 Rust 编程中,主要有以下线程同步的方法: 互斥锁(Mutex) 我们可以使用互斥锁 Mutex 来控制只能有单独一个线程读取/修改 对象。通常实践是在外面加上原子引用计数 Arc 变成 Arc>,来减少 Mutex 拷贝的开销。对于多读少写的场景,可以用 RwLock 提高并发。 条件变量(CondVar) 条件变量用于“阻塞”线程并使得线程在等待事件时不需要消耗 CPU 时间。通常会与放进互斥锁 布尔型的预言值(状态值)关联使用,在状态值发生变化时通知条件变量。 屏障(Barrier) 屏障用于在某个需要若干线程 都完成 前置操作后再开始计算的操作之前,让所有所需线程 的状态都达到能开始进行计算的状态。","breadcrumbs":"记一次面试 » 线程同步有哪些方法?如何用这些方法实现一个 RwLock?","id":"29","title":"线程同步有哪些方法?如何用这些方法实现一个 RwLock?"},"3":{"body":"在最初的讨论过程中,因为我们没有仔细留意插件仓库使用的 encode_wide 是自行做过封装的,因此我们一开始根据 以下代码 进行分析: pub fn init(f: Box>) -> TauriPlugin { plugin::Builder::new(\"single-instance\") .setup(|app| { let app_name = &app.package_info().name; let class_name = format!(\"{}-single-instance-class\", app_name); let window_name = format!(\"{}-single-instance-window\", app_name); let hmutex = unsafe { CreateMutexW( std::ptr::null(), true.into(), encode_wide(\"tauri-plugin-single-instance-mutex\").as_ptr(), ) }; if unsafe { GetLastError() } == ERROR_ALREADY_EXISTS { unsafe { let hwnd = FindWindowW( encode_wide(&class_name).as_ptr(), encode_wide(&window_name).as_ptr(), ); // omitted } // omitted } // omitted }) .on_event(|app, event| { // omitted }) .build()\n} 有 Windows 编程经验的 @PotatoTooLarge 指出,encode_wide(来自 std 的 Window扩展 )并不会补充 \\0 结束符: Re-encodes an OsStr as a wide character sequence, i.e., potentially ill-formed UTF-16. This is lossless: calling OsStringExt::from_wide and then encode_wide on the result will yield the original code units. Note that the encoding does not add a final null terminator. 于是博主听取建议,把所有会传递到 encode_wide 函数的字符串都添加了 \\0,形成了 一版修复 : pub fn init(f: Box>) -> TauriPlugin { plugin::Builder::new(\"single-instance\") .setup(|app| { let app_name = &app.package_info().name; // let class_name = format!(\"{}-single-instance-class\", app_name); let class_name = format!(\"{}-single-instance-class\\0\", app_name); // let window_name = format!(\"{}-single-instance-window\", app_name); let window_name = format!(\"{}-single-instance-window\\0\", app_name); let hmutex = unsafe { CreateMutexW( std::ptr::null(), true.into(), // encode_wide(\"tauri-plugin-single-instance-mutex\").as_ptr(), encode_wide(\"tauri-plugin-single-instance-mutex\\0\").as_ptr(), ) }; if unsafe { GetLastError() } == ERROR_ALREADY_EXISTS { unsafe { let hwnd = FindWindowW( encode_wide(&class_name).as_ptr(), encode_wide(&window_name).as_ptr(), ); // omitted } // omitted } // omitted }) .on_event(|app, event| { // omitted }) .build()\n} 然后使用修复前会引起单例功能失效的 z123456789012345 作为测试用例,验证单例功能可用了,证明该修改可以修复单例功能失效的问题。","breadcrumbs":"由 tauri 单例模式 bug “意外修复” 发现的 dangling » PotatoTooLarge: 传递给 Win32 API 的字符串要用 C string 风格的 \\0 结束","id":"3","title":"PotatoTooLarge: 传递给 Win32 API 的字符串要用 C string 风格的 \\0 结束"},"30":{"body":"这道问题最后我也并没有理解,“生命周期标注无法修正的问题”,字面意思是,即使我们按照我们 期望的程序语义来修正了生命周期标注,这个程序仍然不能通过编译,或者再运行时仍然不能得到 期望结果。按此描述,一个可能的例子是,我们尝试从一个较短的引用返回一个较长的引用: fn longhten<'a>(s_ref: &'a str) -> &'static str { s_ref\n} fn main() { let s = String::from(\"hello\"); let static_ref = longhten(&s); println!(\"{}\", static_ref);\n}","breadcrumbs":"记一次面试 » 有什么问题是生存期标注无法修正的?请给出一个例子","id":"30","title":"有什么问题是生存期标注无法修正的?请给出一个例子"},"31":{"body":"Reactor 作为反应器,上面同时挂载了成千上万个待唤醒的事件,这里使用了mio统一封装了操作系统的多路复用API。 在Linux中使用的是Epoll [1] ,在Mac中使用的则是Kqueue [2] loop { // 轮询事件是否超时 poll.poll(&events, timeout); for event in events.iter() { if (event.is_readable()) { for waker in event.readers.wakers { waker.wake(); } } if (event.is_writeable()) { for waker in event.writers.wakers { waker.wake(); } } }\n}","breadcrumbs":"记一次面试 » Waker 如何被唤醒? Reactor要怎么实现?","id":"31","title":"Waker 如何被唤醒? Reactor要怎么实现?"},"32":{"body":"","breadcrumbs":"记一次面试 » 一面 -- 手写代码","id":"32","title":"一面 -- 手写代码"},"33":{"body":"# use std::cmp::Ordering;\n#\n/// 给出一个从小到大排列的数组,请实现一个函数,用二分法把指定数组 x 的位置找出来。若 x\n/// 不存在,则返回 -1. 若 x 存在多个,请返回 x 在数组中第一次出现的位置\nfn find(arr: Vec, x: i32) -> i32 { let (mut left, mut right) = (0, arr.len() - 1); loop { if left > right { return -1; } let mut mid = (left + right) / 2; match arr[mid].cmp(&x) { // 记得要排除已经命中的元素! Ordering::Less => left = mid + 1, Ordering::Greater => right = mid - 1, Ordering::Equal => { while mid >= 1 && arr[mid - 1] == x { mid -= 1; } return mid as i32; }, } }\n} # #[cfg(test)]\n# mod test {\n# use super::*;\n#\n# #[test]\n# fn should_return_minus1() {\n# let arr = vec![1, 3, 5];\n# let x = 2;\n#\n# assert_eq!(find(arr, x), -1);\n# }\n#\n# #[test]\n# fn should_return_mid() {\n# let arr = vec![1, 3, 5, 7, 9, 10, 10];\n# let x = 7;\n#\n# assert_eq!(find(arr, x), 3);\n# }\n#\n# #[test]\n# fn should_return_first() {\n# let arr = vec![1, 3, 5, 7, 9, 10, 10];\n# let x = 10;\n#\n# assert_eq!(find(arr, x), 5);\n# }\n# } Q: 请分析该函数的算法复杂度? A: 时间复杂度 \\( O(\\log n) \\),最坏情况下的事件复杂度是 \\( O(n) \\) Q: 请优化这个算法? A:一个优化方法是 插值查找法 ,利用如下公式自动根据查找到的元素与目标的距离来修正下一次查找 的区间范围,提高查找速度: \\[ mid = left + { key - arr[left] \\over arr[right] - key } (right - left) \\]","breadcrumbs":"记一次面试 » 1. 实现一个二分查找函数","id":"33","title":"1. 实现一个二分查找函数"},"34":{"body":"请反转二叉树。如给出以下二叉树: 1 / \\ 2 3 / \\ / \\\n4 5 6 7 反转为: 1 / \\ 3 2 / \\ / \\\n7 6 5 4 递归解法: # use std::convert::Into;\n#\nstruct Node { val: i32, left: NodeLink, right: NodeLink,\n} type NodeLink = Option>; # fn construct_tree() -> Box {\n# let l3left1 = Node {\n# val: 4,\n# left: None,\n# right: None,\n# };\n#\n# let l3right1 = Node {\n# val: 5,\n# left: None,\n# right: None,\n# };\n#\n# let l3left2 = Node {\n# val: 6,\n# left: None,\n# right: None,\n# };\n#\n# let l3right2 = Node {\n# val: 7,\n# left: None,\n# right: None,\n# };\n#\n# let l2left = Node {\n# val: 2,\n# left: Some(Box::new(l3left1)),\n# right: Some(Box::new(l3right1)),\n# };\n#\n#\n# let l2right = Node {\n# val: 3,\n# left: Some(Box::new(l3left2)),\n# right: Some(Box::new(l3right2)),\n# };\n#\n# Box::new(Node {\n# val: 1,\n# left: Some(Box::new(l2left)),\n# right: Some(Box::new(l2right)),\n# })\n# }\n#\n# fn construct_mirror() -> Box {\n# let l3left1 = Node {\n# val: 7,\n# left: None,\n# right: None,\n# };\n#\n# let l3right1 = Node {\n# val: 6,\n# left: None,\n# right: None,\n# };\n#\n# let l3left2 = Node {\n# val: 5,\n# left: None,\n# right: None,\n# };\n#\n# let l3right2 = Node {\n# val: 4,\n# left: None,\n# right: None,\n# };\n#\n# let l2left = Node {\n# val: 3,\n# left: Some(Box::new(l3left1)),\n# right: Some(Box::new(l3right1)),\n# };\n#\n#\n# let l2right = Node {\n# val: 2,\n# left: Some(Box::new(l3left2)),\n# right: Some(Box::new(l3right2)),\n# };\n#\n# Box::new(Node {\n# val: 1,\n# left: Some(Box::new(l2left)),\n# right: Some(Box::new(l2right)),\n# })\n# }\n#\n# impl Into> for Box {\n# fn into(mut self) -> Vec {\n# let v_left: Vec;\n# let v_right: Vec;\n# v_left = if let Some(node) = self.left.take() {\n# node.into()\n# } else {\n# Vec::new()\n# };\n#\n# v_right = if let Some(node) = self.right.take() {\n# node.into()\n# } else {\n# Vec::new()\n# };\n#\n# let mut v = Vec::new();\n# v.push(self.val);\n# v.extend(v_left.into_iter());\n# v.extend(v_right.into_iter());\n#\n# v\n# }\n# }\n#\nfn mirror(root: &mut Node) { let (mut tmp_left, mut tmp_right) = (NodeLink::None, NodeLink::None); if let Some(mut node) = root.left.take() { mirror(&mut node); tmp_left = Some(node); } if let Some(mut node) = root.right.take() { mirror(&mut node); tmp_right = Some(node); } root.left = tmp_right; root.right = tmp_left;\n} # #[cfg(test)]\n# mod test {\n# use super::*;\n#\n# #[test]\n# fn should_mirrored() {\n# let mut tree = construct_tree();\n# let expect = construct_mirror();\n#\n# let mirror = mirror(&mut tree);\n# assert_eq!( as Into>>::into(tree), as Into>>::into(expect));\n# }\n# } Q: 请优化这个算法? A:如果不用递归(因为递归会加深调用栈),可以使用 广度优先搜索算法 来自根向叶 逐层反转左右子节点的指针,并将子节点的指针放入到队列中待进行处理。 线程同步 -- 百度百科 Rust异步浅谈 -- leaxoy","breadcrumbs":"记一次面试 » 2. 镜像二叉树","id":"34","title":"2. 镜像二叉树"},"35":{"body":"原文: https://mbuffett.com/posts/bevy-snake-tutorial/#0.3 Bevy 最近普及开来了,但是相关学习资料还是很少。这篇文章尝试提供 Bevy 官方书(The Bevy book)的下一步学习。最后产品看起来像这样: 这大约是 300 行 Rust 代码;也需要花点时间深入。如果你想快进到成品代码,请点 这里 。每一个小节开头都有一份代码差异,这应该会在你不是很清晰哪里需要插入代码的时候更加清晰一点。","breadcrumbs":"用 Bevy 游戏引擎编写贪吃蛇(译) » 用 Bevy 游戏引擎编写贪吃蛇(译)","id":"35","title":"用 Bevy 游戏引擎编写贪吃蛇(译)"},"36":{"body":"点击查看差异 我们现在像 Bevy 官方书那样开始,整一个啥都不干的应用。运行 cargo new bevy-snake, 然后把以下代码放到你的 main.rs : use bevy::prelude::*; fn main() { App::build().run();\n} 我们还需要在 Cargo.toml 将 Bevy 作为依赖添加,因为我(原文作者,下同)知道这个教程之后要干嘛,我们现在也提前添加 rand库吧。 // ... [dependencies]\nbevy = \"0.3.0\"\nrand = \"0.7.3\"","breadcrumbs":"用 Bevy 游戏引擎编写贪吃蛇(译) » 新的空的 Bevy 应用","id":"36","title":"新的空的 Bevy 应用"},"37":{"body":"点击查看差异 我们现在要创建一个2D游戏,需要很多不同的系统;用来创建窗口的,用来做渲染循环的,用来处理输出的,用来处理精灵(sprites)的,等等。幸运的是,Bevy的默认插件给了我们以上所有选项: fn main() { App::build().add_plugins(DefaultPlugins).run();\n} 然而 Bevy 的默认插件不包括摄像机(camera),所以我们来插入一个 2D 摄像机,只要我们创建我们第一个系统就可以设置了: fn setup(mut commands: Commands) { commands.spawn(Camera2dComponents::default());\n} Cammands 通常用来排列命令,来更改游戏世界与资源。在这里,我们创建一个带有 2D 摄像机组件的实体。为Bevy的魔法做点准备吧: App::build() .add_startup_system(setup.system()) // <-- .add_plugins(DefaultPlugins) .run(); 我们需要做的只是在我们的函数是调用 .system(),然后 Bevy 会神奇地在启动地时候调用 commands 参数。再运行一次 app, 你应该能看到一个像这样的空窗口:","breadcrumbs":"用 Bevy 游戏引擎编写贪吃蛇(译) » 创建窗口","id":"37","title":"创建窗口"},"38":{"body":"点击查看差异 我们来写个蛇头放在窗口上吧。我们先定义几个结构体: struct SnakeHead;\nstruct Materials { head_material: Handle,\n} SnakeHead 仅仅是一个空结构体,我们会把它当作一个组件来使用,它就是像某种标签,我们会放到一个实体上,之后我们能通过查询带有 SnakeHead 组件的实体来找到这个实体。像这样的空结构体在 Bevy 中是一种常见的模式,组件经常不需要他们自己的任何状态。 Materials 以后会变成一种资源,用来存储我们给蛇头使用的材质,也会用来存储蛇身和食物的材质。 head_material 句柄应该在游戏设置的时候就应该创建好,所以我们接下来要做的是,修改我们的 setup 函数: fn setup(mut commands: Commands, mut materials: ResMut>) { commands.spawn(Camera2dComponents::default()); commands.insert_resource(Materials { head_material: materials.add(Color::rgb(0.7, 0.7, 0.7).into()), });\n} 注意: Bevy要求在注册系统时按照特定的顺序。命令(Commands) -> 资源(Resources) -> 组件(Components)/查询(Queries)。如果你在弄乱一个系统之后获得一个神秘的编译时错误,请检查你的顺序。 materials.add 会返回 Handle。我们创建了使用这个新建 handle 的 Materials 结构体。之后,我们尝试访问类型为 Materials 的资源, Bevy会找到我们这个结构体。现在我们来在新的系统里创建我们的蛇头实体,然后你会看到我们如何使用前述资源的: fn game_setup(mut commands: Commands, materials: Res) { commands .spawn(SpriteComponents { material: materials.head_material.clone(), sprite: Sprite::new(Vec2::new(10.0, 10.0)), ..Default::default() }) .with(SnakeHead);\n} 现在我们有了新的系统,它会寻找类型为 Materials 的资源。它也会创建(spawn)一个新实体,带有 SpriteComponents 和 SnakeHead 组件。为了创建 SpriteComponents, 我们将我们之间创建的颜色的 handle 传入,并且给精灵 10x10 的大小。我们将这个系统添加到我们 app 的构建器: .add_startup_system(setup.system())\n.add_startup_stage(\"game_setup\") // <--\n.add_startup_system_to_stage(\"game_setup\", game_setup.system()) // <-- 我们需要一个新的场景而不是再一次调用 add_startup_system 的原因是,我们需要使用在 setup 函数中插入的资源。这次运行后,你应该在屏幕中央看到蛇头: 好了,可能我们叫它“蛇头”有点过了,你可以看到一个 10x10 的白色精灵。","breadcrumbs":"用 Bevy 游戏引擎编写贪吃蛇(译) » 开始编写一条蛇","id":"38","title":"开始编写一条蛇"},"39":{"body":"点击查看差异 如果小蛇不运动,那么游戏很无趣,所以我们先让蛇头动起来。我们之后再担心输入,现在我们的目标是让蛇头移动。所以我们来创建一个系统来移动所有的蛇头: fn snake_movement(mut head_positions: Query<(&SnakeHead, &mut Transform)>) { for (_head, mut transform) in head_positions.iter_mut() { *transform.translation.y_mut() += 2.; }\n} 这里有个新概念, Query 类型。我们用它来迭代所有拥有 SnakeHead 组件以及 Transform 组件的实体。我们不需要担心实际上如何创建查询类型, bevy 会帮我们创建好并用它调用我们的函数,算是 ECS 魔法的一部分。所以我们来加上这个系统, 然后看看会发生些什么: .add_startup_system_to_stage(\"game_setup\", game_setup.system())\n.add_system(snake_movement.system()) // <--\n.add_plugins(DefaultPlugins) 这是我们看到的,一头蛇移出了屏幕: 你可能再思考 Transform 组件。当我们生成 SnakeHead 时,我们并没有给它 Transform,所以我们怎么就能找到一个同事拥有 SnakeHead 和 Transform 组件的实体呢?实际上 SpriteComponents 是一捆组件。就 SpriteComponents 来说,它包含了 Transform 组件,以及一堆其他组件(如 Sprite, Mesh, Draw, Rotation, Sale)。","breadcrumbs":"用 Bevy 游戏引擎编写贪吃蛇(译) » 移动小蛇","id":"39","title":"移动小蛇"},"4":{"body":"在博主提交了修复后,插件仓库作者提醒,前文所述的代码使用的 encode_wide 是 封装拼接了 \\0 后再传递参数的: pub fn encode_wide(string: impl AsRef) -> Vec { std::os::windows::prelude::OsStrExt::encode_wide(string.as_ref()) .chain(std::iter::once(0)) .collect()\n} 这意味着,并不是 \\0 导致问题的失效,因为该函数在 windows 环境下执行是能够补足 \\0 的: fn encode_wide(string: impl AsRef) -> Vec { std::os::windows::prelude::OsStrExt::encode_wide(string.as_ref()) .chain(std::iter::once(0)) .collect()\n} fn main() { let product_name = \"z123456789012345\"; // output: [122, 49, 50, 51, 52, 53, 54, 55, 56, 57, 48, 49, 50, 51, 52, 53, 0] // ^ // null concated here so it's null-terminated --| println!(\"{:?}\", encode_wide(product_name));\n}","breadcrumbs":"由 tauri 单例模式 bug “意外修复” 发现的 dangling » 但它并不是真的修复","id":"4","title":"但它并不是真的修复"},"40":{"body":"我们来修改我们小蛇的移动系统,使得我们可以控制小蛇: fn snake_movement( keyboard_input: Res>, mut head_positions: Query>,\n) { for mut transform in head_positions.iter_mut() { if keyboard_input.pressed(KeyCode::Left) { *transform.translation.x_mut() -= 2.; } if keyboard_input.pressed(KeyCode::Right) { *transform.translation.x_mut() += 2.; } if keyboard_input.pressed(KeyCode::Down) { *transform.translation.y_mut() -= 2.; } if keyboard_input.pressed(KeyCode::Up) { *transform.translation.y_mut() += 2.; } }\n} 留意到我们的查询 Query<(&SnakeHead, &mut Transform)> 改为了 Query>,其实当前版本没有必要更改,旧的查询依然能很好地工作。我想,第一个系统的类型签名可能简单些,但是现在我们用正确的方式编写类型。这写法更正确是因为我们其实不需要 SnakeHead 组件。所以 With 类型允许我们说,“我们需要那些有蛇头的实体,但是我不关心蛇头组件,只给我 transform 组件就好。”每个系统访问的组件越少,bevy就能并行越多的系统。例如,如果另外一个系统正在修改 SnakeHead 组件,那这个系统旧不能在用旧写法的时候并行了。 现在,我们能控制小蛇了,尽管它动起来不那么像蛇:","breadcrumbs":"用 Bevy 游戏引擎编写贪吃蛇(译) » 控制小蛇","id":"40","title":"控制小蛇"},"41":{"body":"点击查看差异 到现在我们一直在用窗口的坐标,但这种方法只能在 (0, 0) 坐标在窗口正中央,并且单位是像素的时候有效。贪吃蛇游戏通常用格子,所以如果我们把我们的贪吃蛇设置成 10x10,那我们的窗口会 真的 很小。我们让日子变得轻松些吧,我们选择用我们自己的位置和尺寸。然后,我们用系统来处理变换到窗口的坐标。 我们先定义格子为 10x10。在程序文件开头定义如下变量: const ARENA_WIDTH: u32 = 10;\nconst ARENA_HEIGHT: u32 = 10; 以及我们用于处理位置/尺寸的结构体: #[derive(Default, Copy, Clone, Eq, PartialEq, Hash)]\nstruct Position { x: i32, y: i32,\n} struct Size { width: f32, height: f32,\n}\nimpl Size { pub fn square(x: f32) -> Self { Self { width: x, height: x, } }\n} 相对直接地,有一个辅助方法来获取一个有相等长宽的 Size. Position 派生了一些很有用的 trait,所以我们不必不停地回顾这个结构体。 Size 可以仅仅包含一个浮点数,因为所有的对象最后都有相等的长度和宽度,但是我给它长度和宽度好像有点不对。我们现在把这些组件添加到我们生成的蛇头上: commands .spawn(SpriteComponents { material: materials.head_material.clone(), sprite: Sprite::new(Vec2::new(10.0, 10.0)), ..Default::default() }) .with(SnakeHead) .with(Position { x: 3, y: 3 }) // <-- .with(Size::square(0.8)); // <-- 这些组件暂时不做任何事情,我们现在就来将我们的尺寸映射到精灵的尺寸: fn size_scaling(windows: Res, mut q: Query<(&Size, &mut Sprite)>) { let window = windows.get_primary().unwrap(); for (sprite_size, mut sprite) in q.iter_mut() { sprite.size = Vec2::new( sprite_size.width / ARENA_WIDTH as f32 * window.width() as f32, sprite_size.height / ARENA_HEIGHT as f32 * window.height() as f32, ); }\n} 这个尺寸变换逻辑是这样的:如果某个对象有一个单位格子宽度,格子宽40,然后窗口现在 400px 宽,那么它应该有10哥宽度。下面我们做位置系统: fn position_translation(windows: Res, mut q: Query<(&Position, &mut Transform)>) { fn convert(pos: f32, bound_window: f32, bound_game: f32) -> f32 { let tile_size = bound_window / bound_game; pos / bound_game * bound_window - (bound_window / 2.) + (tile_size / 2.) } let window = windows.get_primary().unwrap(); for (pos, mut transform) in q.iter_mut() { transform.translation = Vec3::new( convert(pos.x as f32, window.width() as f32, ARENA_WIDTH as f32), convert(pos.y as f32, window.height() as f32, ARENA_HEIGHT as f32), 0.0, ); }\n} 位置变换:如果项目的 X 坐标在我们的系统中是 5,宽度是 10,并且窗口宽度是200,那么坐标应该是 5/10 * 200 - 200 / 2。我们减去一半的窗口宽度,因为我们的做消息是从左下角开始,然后替换到正中央。然后我们再加上半个格子,因为我们想要我们精灵的左下角对齐格子的左下角,而不是精灵中心对齐。 然后我们把这些系统加到我们的应用构建器上: .add_system(snake_movement.system())\n.add_system(position_translation.system()) <--\n.add_system(size_scaling.system()) <--\n.add_plugins(DefaultPlugins)\n.run(); 注意: 现在最明显的问题是小蛇被压扁了。另外一个问题是我们破环了我们的输入处理。我们先修复输入处理,然后我们得记得回来处理我们被压扁的小蛇,把它恢复原状。","breadcrumbs":"用 Bevy 游戏引擎编写贪吃蛇(译) » 码格子","id":"41","title":"码格子"},"42":{"body":"点击查看差异 我们现在配置好了格子坐标,现在我们需要更新我们的 snake_movement 系统。之前我们使用 Transform 的地方,现在替换成 Position: fn snake_movement( keyboard_input: Res>, mut head_positions: Query>,\n) { for mut pos in head_positions.iter_mut() { if keyboard_input.pressed(KeyCode::Left) { pos.x -= 1; } if keyboard_input.pressed(KeyCode::Right) { pos.x += 1; } if keyboard_input.pressed(KeyCode::Down) { pos.y -= 1; } if keyboard_input.pressed(KeyCode::Up) { pos.y += 1; } }\n}","breadcrumbs":"用 Bevy 游戏引擎编写贪吃蛇(译) » 使用我们的格子","id":"42","title":"使用我们的格子"},"43":{"body":"[1] 点击查看差异 我们上一步中的小蛇被压扁了,是因为默认的窗口尺寸并不是方形的,然而我们的格子是,所以我们每个格坐标会宽度长于高度。我们修复它最简单的方法,是在构建 app 的时候创建一个 WindowDescriptor 资源: App::build() .add_resource(WindowDescriptor { // <-- title: \"Snake!\".to_string(), // <-- width: 200, // <-- height: 200, // <-- ..Default::default() // <-- }) .add_startup_system(setup.system()) 同时,我们改一下背景颜色,插入这个 use 语句来引入 ClearColor 结构体: use bevy::render::pass::ClearColor; 然后在 app 构建器增加资源: .add_resource(ClearColor(Color::rgb(0.04, 0.04, 0.04))) 原文中这里的规格是 2000,但是 2000 的规则放 10x10 显然太大了, 这里改成 200","breadcrumbs":"用 Bevy 游戏引擎编写贪吃蛇(译) » 调整窗口大小","id":"43","title":"调整窗口大小"},"44":{"body":"现在我们的小蛇可以到处移动了,该喂点东西给它了。现在我们给 Materials 加一个 food_materials 字段: struct Materials { head_material: Handle, food_material: Handle, // <--\n} 然后把这个新材质加到我们的 setup 函数里: commands.insert_resource(Materials { head_material: materials.add(Color::rgb(0.7, 0.7, 0.7).into()), food_material: materials.add(Color::rgb(1.0, 0.0, 1.0).into()), // <--\n}); 然后我们需要 Duration 给要创建的定时器使用,而且我们还需要 random 来随机分配食物的位置。先在程序里引入这些: use rand::prelude::random;\nuse std::time::Duration; 然后我们因素两个新结构体: Food 组件让我们知道哪个实体是食物,以及一个定时制造食物的定时器: struct Food; struct FoodSpawnTimer(Timer);\nimpl Default for FoodSpawnTimer { fn default() -> Self { Self(Timer::new(Duration::from_millis(1000), true)) }\n} 至于实现 Default 的原因,会在我解释下面的系统的时候说明: fn food_spawner( mut commands: Commands, materials: Res, time: Res

+ +
+ +
+ + + + + + + + +
+
+ +

Bevy 游戏引擎编写贪吃蛇(译)

+
+

原文:https://mbuffett.com/posts/bevy-snake-tutorial/#0.3

+
+

Bevy 最近普及开来了,但是相关学习资料还是很少。这篇文章尝试提供 Bevy 官方书(The Bevy book)的下一步学习。最后产品看起来像这样:

+ +

这大约是 300 行 Rust 代码;也需要花点时间深入。如果你想快进到成品代码,请点 这里。每一个小节开头都有一份代码差异,这应该会在你不是很清晰哪里需要插入代码的时候更加清晰一点。

+

新的空的 Bevy 应用

+
+

点击查看差异

+
+

我们现在像 Bevy 官方书那样开始,整一个啥都不干的应用。运行 cargo new bevy-snake, 然后把以下代码放到你的 main.rs

+
use bevy::prelude::*;
+
+fn main() {
+    App::build().run();
+}
+
+

我们还需要在 Cargo.toml 将 Bevy 作为依赖添加,因为我(原文作者,下同)知道这个教程之后要干嘛,我们现在也提前添加 rand库吧。

+
// ...
+
+[dependencies]
+bevy = "0.3.0"
+rand = "0.7.3"
+
+

创建窗口

+
+

点击查看差异

+
+

我们现在要创建一个2D游戏,需要很多不同的系统;用来创建窗口的,用来做渲染循环的,用来处理输出的,用来处理精灵(sprites)的,等等。幸运的是,Bevy的默认插件给了我们以上所有选项:

+
fn main() {
+    App::build().add_plugins(DefaultPlugins).run();
+}
+
+

然而 Bevy 的默认插件不包括摄像机(camera),所以我们来插入一个 2D 摄像机,只要我们创建我们第一个系统就可以设置了:

+
fn setup(mut commands: Commands) {
+    commands.spawn(Camera2dComponents::default());
+}
+
+

Cammands 通常用来排列命令,来更改游戏世界与资源。在这里,我们创建一个带有 2D 摄像机组件的实体。为Bevy的魔法做点准备吧:

+
App::build()
+    .add_startup_system(setup.system()) // <--
+    .add_plugins(DefaultPlugins)
+    .run();
+
+

我们需要做的只是在我们的函数是调用 .system(),然后 Bevy 会神奇地在启动地时候调用 commands 参数。再运行一次 app, 你应该能看到一个像这样的空窗口:

+

+

开始编写一条蛇

+
+

点击查看差异

+
+

我们来写个蛇头放在窗口上吧。我们先定义几个结构体:

+
struct SnakeHead;
+struct Materials {
+    head_material: Handle<ColorMaterial>,
+}
+
+

SnakeHead 仅仅是一个空结构体,我们会把它当作一个组件来使用,它就是像某种标签,我们会放到一个实体上,之后我们能通过查询带有 SnakeHead 组件的实体来找到这个实体。像这样的空结构体在 Bevy 中是一种常见的模式,组件经常不需要他们自己的任何状态。 Materials 以后会变成一种资源,用来存储我们给蛇头使用的材质,也会用来存储蛇身和食物的材质。

+

head_material 句柄应该在游戏设置的时候就应该创建好,所以我们接下来要做的是,修改我们的 setup 函数:

+
fn setup(mut commands: Commands, mut materials: ResMut<Assets<ColorMaterial>>) {
+    commands.spawn(Camera2dComponents::default());
+    commands.insert_resource(Materials {
+        head_material: materials.add(Color::rgb(0.7, 0.7, 0.7).into()),
+    });
+}
+
+
+

注意: Bevy要求在注册系统时按照特定的顺序。命令(Commands) -> 资源(Resources) -> 组件(Components)/查询(Queries)。如果你在弄乱一个系统之后获得一个神秘的编译时错误,请检查你的顺序。

+
+

materials.add 会返回 Handle<ColorMaterial>。我们创建了使用这个新建 handle 的 Materials 结构体。之后,我们尝试访问类型为 Materials 的资源, Bevy会找到我们这个结构体。现在我们来在新的系统里创建我们的蛇头实体,然后你会看到我们如何使用前述资源的:

+
fn game_setup(mut commands: Commands, materials: Res<Materials>) {
+    commands
+        .spawn(SpriteComponents {
+            material: materials.head_material.clone(),
+            sprite: Sprite::new(Vec2::new(10.0, 10.0)),
+            ..Default::default()
+        })
+        .with(SnakeHead);
+}
+
+

现在我们有了新的系统,它会寻找类型为 Materials 的资源。它也会创建(spawn)一个新实体,带有 SpriteComponentsSnakeHead 组件。为了创建 SpriteComponents, 我们将我们之间创建的颜色的 handle 传入,并且给精灵 10x10 的大小。我们将这个系统添加到我们 app 的构建器:

+
.add_startup_system(setup.system())
+.add_startup_stage("game_setup") // <--
+.add_startup_system_to_stage("game_setup", game_setup.system()) // <--
+
+

我们需要一个新的场景而不是再一次调用 add_startup_system 的原因是,我们需要使用在 setup 函数中插入的资源。这次运行后,你应该在屏幕中央看到蛇头:

+

+

好了,可能我们叫它“蛇头”有点过了,你可以看到一个 10x10 的白色精灵。

+

移动小蛇

+
+

点击查看差异

+
+

如果小蛇不运动,那么游戏很无趣,所以我们先让蛇头动起来。我们之后再担心输入,现在我们的目标是让蛇头移动。所以我们来创建一个系统来移动所有的蛇头:

+
fn snake_movement(mut head_positions: Query<(&SnakeHead, &mut Transform)>) {
+    for (_head, mut transform) in head_positions.iter_mut() {
+        *transform.translation.y_mut() += 2.;
+    }
+}
+
+

这里有个新概念, Query 类型。我们用它来迭代所有拥有 SnakeHead 组件以及 Transform 组件的实体。我们不需要担心实际上如何创建查询类型, bevy 会帮我们创建好并用它调用我们的函数,算是 ECS 魔法的一部分。所以我们来加上这个系统, 然后看看会发生些什么:

+
.add_startup_system_to_stage("game_setup", game_setup.system())
+.add_system(snake_movement.system()) // <--
+.add_plugins(DefaultPlugins)
+
+

这是我们看到的,一头蛇移出了屏幕:

+ +

你可能再思考 Transform 组件。当我们生成 SnakeHead 时,我们并没有给它 Transform,所以我们怎么就能找到一个同事拥有 SnakeHeadTransform 组件的实体呢?实际上 SpriteComponents 是一捆组件。就 SpriteComponents 来说,它包含了 Transform 组件,以及一堆其他组件(如 Sprite, Mesh, Draw, Rotation, Sale)。

+

控制小蛇

+

我们来修改我们小蛇的移动系统,使得我们可以控制小蛇:

+
fn snake_movement(
+    keyboard_input: Res<Input<KeyCode>>,
+    mut head_positions: Query<With<SnakeHead, &mut Transform>>,
+) {
+    for mut transform in head_positions.iter_mut() {
+        if keyboard_input.pressed(KeyCode::Left) {
+            *transform.translation.x_mut() -= 2.;
+        }
+        if keyboard_input.pressed(KeyCode::Right) {
+            *transform.translation.x_mut() += 2.;
+        }
+        if keyboard_input.pressed(KeyCode::Down) {
+            *transform.translation.y_mut() -= 2.;
+        }
+        if keyboard_input.pressed(KeyCode::Up) {
+            *transform.translation.y_mut() += 2.;
+        }
+    }
+}
+
+

留意到我们的查询 Query<(&SnakeHead, &mut Transform)> 改为了 Query<With<SnakeHead, &mut Transform>>,其实当前版本没有必要更改,旧的查询依然能很好地工作。我想,第一个系统的类型签名可能简单些,但是现在我们用正确的方式编写类型。这写法更正确是因为我们其实不需要 SnakeHead 组件。所以 With 类型允许我们说,“我们需要那些有蛇头的实体,但是我不关心蛇头组件,只给我 transform 组件就好。”每个系统访问的组件越少,bevy就能并行越多的系统。例如,如果另外一个系统正在修改 SnakeHead 组件,那这个系统旧不能在用旧写法的时候并行了。

+

现在,我们能控制小蛇了,尽管它动起来不那么像蛇:

+ +

码格子

+
+

点击查看差异

+
+

到现在我们一直在用窗口的坐标,但这种方法只能在 (0, 0) 坐标在窗口正中央,并且单位是像素的时候有效。贪吃蛇游戏通常用格子,所以如果我们把我们的贪吃蛇设置成 10x10,那我们的窗口会 真的 很小。我们让日子变得轻松些吧,我们选择用我们自己的位置和尺寸。然后,我们用系统来处理变换到窗口的坐标。

+

我们先定义格子为 10x10。在程序文件开头定义如下变量:

+
const ARENA_WIDTH: u32 = 10;
+const ARENA_HEIGHT: u32 = 10;
+
+

以及我们用于处理位置/尺寸的结构体:

+
#[derive(Default, Copy, Clone, Eq, PartialEq, Hash)]
+struct Position {
+    x: i32,
+    y: i32,
+}
+
+struct Size {
+    width: f32,
+    height: f32,
+}
+impl Size {
+    pub fn square(x: f32) -> Self {
+        Self {
+            width: x,
+            height: x,
+        }
+    }
+}
+
+

相对直接地,有一个辅助方法来获取一个有相等长宽的 Size. Position 派生了一些很有用的 trait,所以我们不必不停地回顾这个结构体。 Size 可以仅仅包含一个浮点数,因为所有的对象最后都有相等的长度和宽度,但是我给它长度和宽度好像有点不对。我们现在把这些组件添加到我们生成的蛇头上:

+
commands
+    .spawn(SpriteComponents {
+        material: materials.head_material.clone(),
+        sprite: Sprite::new(Vec2::new(10.0, 10.0)),
+        ..Default::default()
+    })
+    .with(SnakeHead)
+    .with(Position { x: 3, y: 3 }) // <--
+    .with(Size::square(0.8)); // <--
+
+

这些组件暂时不做任何事情,我们现在就来将我们的尺寸映射到精灵的尺寸:

+
fn size_scaling(windows: Res<Windows>, mut q: Query<(&Size, &mut Sprite)>) {
+    let window = windows.get_primary().unwrap();
+    for (sprite_size, mut sprite) in q.iter_mut() {
+        sprite.size = Vec2::new(
+            sprite_size.width / ARENA_WIDTH as f32 * window.width() as f32,
+            sprite_size.height / ARENA_HEIGHT as f32 * window.height() as f32,
+        );
+    }
+}
+
+

这个尺寸变换逻辑是这样的:如果某个对象有一个单位格子宽度,格子宽40,然后窗口现在 400px 宽,那么它应该有10哥宽度。下面我们做位置系统:

+
fn position_translation(windows: Res<Windows>, mut q: Query<(&Position, &mut Transform)>) {
+    fn convert(pos: f32, bound_window: f32, bound_game: f32) -> f32 {
+        let tile_size = bound_window / bound_game;
+        pos / bound_game * bound_window - (bound_window / 2.) + (tile_size / 2.)
+    }
+    let window = windows.get_primary().unwrap();
+    for (pos, mut transform) in q.iter_mut() {
+        transform.translation = Vec3::new(
+            convert(pos.x as f32, window.width() as f32, ARENA_WIDTH as f32),
+            convert(pos.y as f32, window.height() as f32, ARENA_HEIGHT as f32),
+            0.0,
+        );
+    }
+}
+
+ +

位置变换:如果项目的 X 坐标在我们的系统中是 5,宽度是 10,并且窗口宽度是200,那么坐标应该是 5/10 * 200 - 200 / 2。我们减去一半的窗口宽度,因为我们的做消息是从左下角开始,然后替换到正中央。然后我们再加上半个格子,因为我们想要我们精灵的左下角对齐格子的左下角,而不是精灵中心对齐。

+

然后我们把这些系统加到我们的应用构建器上:

+
.add_system(snake_movement.system())
+.add_system(position_translation.system()) <--
+.add_system(size_scaling.system()) <--
+.add_plugins(DefaultPlugins)
+.run();
+
+
+

注意: 现在最明显的问题是小蛇被压扁了。另外一个问题是我们破环了我们的输入处理。我们先修复输入处理,然后我们得记得回来处理我们被压扁的小蛇,把它恢复原状。

+
+

+

使用我们的格子

+
+

点击查看差异

+
+

我们现在配置好了格子坐标,现在我们需要更新我们的 snake_movement 系统。之前我们使用 Transform 的地方,现在替换成 Position

+
fn snake_movement(
+    keyboard_input: Res<Input<KeyCode>>,
+    mut head_positions: Query<With<SnakeHead, &mut Position>>,
+) {
+    for mut pos in head_positions.iter_mut() {
+        if keyboard_input.pressed(KeyCode::Left) {
+            pos.x -= 1;
+        }
+        if keyboard_input.pressed(KeyCode::Right) {
+            pos.x += 1;
+        }
+        if keyboard_input.pressed(KeyCode::Down) {
+            pos.y -= 1;
+        }
+        if keyboard_input.pressed(KeyCode::Up) {
+            pos.y += 1;
+        }
+    }
+}
+
+ +

调整窗口大小1

+
+

点击查看差异

+
+

我们上一步中的小蛇被压扁了,是因为默认的窗口尺寸并不是方形的,然而我们的格子是,所以我们每个格坐标会宽度长于高度。我们修复它最简单的方法,是在构建 app 的时候创建一个 WindowDescriptor 资源:

+
    App::build()
+        .add_resource(WindowDescriptor { // <--
+            title: "Snake!".to_string(), // <--
+            width: 200,                 // <--
+            height: 200,                // <--
+            ..Default::default()         // <--
+        })
+        .add_startup_system(setup.system())
+
+

同时,我们改一下背景颜色,插入这个 use 语句来引入 ClearColor 结构体:

+
use bevy::render::pass::ClearColor;
+
+

然后在 app 构建器增加资源:

+
.add_resource(ClearColor(Color::rgb(0.04, 0.04, 0.04)))
+
+
1 +

原文中这里的规格是 2000,但是 2000 的规则放 10x10 显然太大了, 这里改成 200

+
+ +

生成食物

+

现在我们的小蛇可以到处移动了,该喂点东西给它了。现在我们给 Materials 加一个 food_materials 字段:

+
struct Materials {
+    head_material: Handle<ColorMaterial>,
+    food_material: Handle<ColorMaterial>, // <--
+}
+
+

然后把这个新材质加到我们的 setup 函数里:

+
commands.insert_resource(Materials {
+    head_material: materials.add(Color::rgb(0.7, 0.7, 0.7).into()),
+    food_material: materials.add(Color::rgb(1.0, 0.0, 1.0).into()), // <--
+});
+
+

然后我们需要 Duration 给要创建的定时器使用,而且我们还需要 random 来随机分配食物的位置。先在程序里引入这些:

+
use rand::prelude::random;
+use std::time::Duration;
+
+

然后我们因素两个新结构体: Food 组件让我们知道哪个实体是食物,以及一个定时制造食物的定时器:

+
struct Food;
+
+struct FoodSpawnTimer(Timer);
+impl Default for FoodSpawnTimer {
+    fn default() -> Self {
+        Self(Timer::new(Duration::from_millis(1000), true))
+    }
+}
+
+

至于实现 Default 的原因,会在我解释下面的系统的时候说明:

+
fn food_spawner(
+    mut commands: Commands,
+    materials: Res<Materials>,
+    time: Res<Time>,
+    mut timer: Local<FoodSpawnTimer>,
+) {
+    timer.0.tick(time.delta_seconds);
+    if timer.0.finished {
+        commands
+            .spawn(SpriteComponents {
+                material: materials.food_material.clone(),
+                ..Default::default()
+            })
+            .with(Food)
+            .with(Position {
+                x: (random::<f32>() * ARENA_WIDTH as f32) as i32,
+                y: (random::<f32>() * ARENA_HEIGHT as f32) as i32,
+            })
+            .with(Size::square(0.8));
+    }
+}
+
+

我们引入了局部资源概念,具体而言是 timer 参数。 Bevy 会看到这个参数并且实例化一个 FoodSpawnTimer 类型的值,用的是我们的 Default 实现。这会在这个系统第一次运行是发生,之后这个系统会一直重用相同的定时器。像这样使用局部资源要比手动注册资源更贴近工程化。这个定时器会一直重复,所以我们只需要调用 tick 函数,然后无论这个系统在定时器完成后什么时候跑,我们就随机创建一些食物。

+

你可能知道下一步是什么了,把这个系统加到应用构建器上:

+
.add_system(food_spawner.system())
+
+

现在我们的程序看起来像这样:

+ +

更像蛇的移动

+
+

点击查看差异

+
+

我们现在准备定时触发小蛇移动。具体说来,我们想小蛇一直在移动,无论我们是否按下按键;并且我们想要它每隔 X 秒移动一次,而不是每一帧都移动。我们会改动几个地方,所以如果你不太清楚要改动哪里,查看这一小节的差异吧。

+

首先,我们需要加一个方向枚举:

+
#[derive(PartialEq, Copy, Clone)]
+enum Direction {
+    Left,
+    Up,
+    Right,
+    Down,
+}
+
+impl Direction {
+    fn opposite(self) -> Self {
+        match self {
+            Self::Left => Self::Right,
+            Self::Right => Self::Left,
+            Self::Up => Self::Down,
+            Self::Down => Self::Up,
+        }
+    }
+}
+
+

然后把这个方向枚举加到我们的 SnakeHead 结构体,使得它知道应该要往哪里移动:

+
struct SnakeHead {
+    direction: Direction,
+}
+
+

我们也得在实例化 SnakeHead 组件的时候给定初始方向,例如我们让它一开始往上走:

+
.with(SnakeHead {
+    direction: Direction::Up,
+})
+
+

小蛇通常移动不是很流畅,是一种一步步来的行动。就行我们生成食物的时候,我们需要使用定时器来让系统没每隔 X秒/毫秒才跑一次。我们需要创建一个结构体来持有定时器:

+
struct SnakeMoveTimer(Timer);
+
+

然后我们把它当成资源加到我们的 app 构建器:

+
.add_resource(SnakeMoveTimer(Timer::new(
+    Duration::from_millis(150. as u64),
+    true,
+)))
+
+

我们之所以不把这个定时器像生成食物的时候把定时器看成局部资源,是因为我们将会在几个系统里用上它,所以我帮你节约了一些重构的工作。因为我们需要在几个系统里使用它,我们需要创建一个新系统来触发这个定时器:

+
fn snake_timer(time: Res<Time>, mut snake_timer: ResMut<SnakeMoveTimer>) {
+    snake_timer.0.tick(time.delta_seconds);
+}
+
+

我们也可以把这段触发逻辑直接放到 snake_movement 系统里,但是我比较喜欢整洁地吧它放到一个单独的系统中,因为这个定时器会用在几个地方。我们把这个系统也加到 app上:

+
.add_system(snake_timer.system())
+
+

现在我们可以做方向逻辑的核心部分,也就是 snake_movement 系统,以下是更新后的版本:

+
fn snake_movement(
+    keyboard_input: Res<Input<KeyCode>>,
+    snake_timer: ResMut<SnakeMoveTimer>,
+    mut heads: Query<(Entity, &mut SnakeHead)>,
+    mut positions: Query<&mut Position>,
+) {
+    if let Some((head_entity, mut head)) = heads.iter_mut().next() {
+        let mut head_pos = positions.get_mut(head_entity).unwrap();
+        let dir: Direction = if keyboard_input.pressed(KeyCode::Left) {
+            Direction::Left
+        } else if keyboard_input.pressed(KeyCode::Down) {
+            Direction::Down
+        } else if keyboard_input.pressed(KeyCode::Up) {
+            Direction::Up
+        } else if keyboard_input.pressed(KeyCode::Right) {
+            Direction::Right
+        } else {
+            head.direction
+        };
+        if dir != head.direction.opposite() {
+            head.direction = dir;
+        }
+        if !snake_timer.0.finished {
+            return;
+        }
+        match &head.direction {
+            Direction::Left => {
+                head_pos.x -= 1;
+            }
+            Direction::Right => {
+                head_pos.x += 1;
+            }
+            Direction::Up => {
+                head_pos.y += 1;
+            }
+            Direction::Down => {
+                head_pos.y -= 1;
+            }
+        };
+    }
+}
+
+

这里没有什么新概念,仅仅是游戏逻辑。你可能在想为什么我们需要获取拥有 SankeHead 组件的 Entity, 然后用另外一个独立的查询来获取位置, 而不是用像 Query<Entity, &SnakeHead, &mut Position> 这样的参数。原因在于,我们之后可能需要其他实体的位置,而分开两个查询访问相同的组件是不会允许放在 Bevy app 构建器上的。这样改了之后,你会获得一个蛇头移动的稍微……像蛇一样:

+ +

加个尾巴

+
+

点击查看差异

+
+

小蛇的尾巴有点复杂。对于每蛇尾的分段,我们需要知道它下一步需要到哪里。我们准备这样实现:将这些分段放到 Vec,然后存储为资源。这样,当我们更新分段的位置时,我们能够迭代所有的分段并且设置每个分段的位置为前一个分段的位置。

+

我们加一个 segment_material 字段到我们趁手的 Materials 结构体:

+
struct Materials {
+    head_material: Handle<ColorMaterial>,
+    segment_material: Handle<ColorMaterial>, // <--
+    food_material: Handle<ColorMaterial>,
+}
+
+

老调重弹,把 segment_material 加到 setup 中:

+
commands.insert_resource(Materials {
+    head_material: materials.add(Color::rgb(0.7, 0.7, 0.7).into()),
+    segment_material: materials.add(Color::rgb(0.3, 0.3, 0.3).into()), // <--
+    food_material: materials.add(Color::rgb(1.0, 0.0, 1.0).into()),
+});
+
+

然后一个给蛇身分段的组件:

+
struct SnakeSegment;
+
+

然后我们再加上我们说到的,用来存储分段列表的资源:

+
#[derive(Default)]
+struct SnakeSegments(Vec<Entity>);
+
+

再把它作为资源加到我们的 app 上:

+
.add_resource(SnakeSegments::default())
+
+

我们我们需要从几个地方生成分段(当你吃食物或者你初始化小蛇的时候),我们需要先创建一个辅助函数:

+
fn spawn_segment(
+    commands: &mut Commands,
+    material: &Handle<ColorMaterial>,
+    position: Position,
+) -> Entity {
+    commands
+        .spawn(SpriteComponents {
+            material: material.clone(),
+            ..SpriteComponents::default()
+        })
+        .with(SnakeSegment)
+        .with(position)
+        .with(Size::square(0.65))
+        .current_entity()
+        .unwrap()
+}
+
+

这看上去非常像我们生成 SnakeHead 的函数,但是替换了 SnakeHead 组件,我们用的是 SnakeSegment 组件。这里要说的新知识点,就是我们最后通过 current_entity 函数,获取了生成的 Entity (其实只是个 id),然后将它返回给调用者以便使用它。现在,我们需要修改我们的游戏配置函数。并非只是生成一个蛇头,它现在要生成一个蛇身的分段:

+
fn spawn_snake(
+    mut commands: Commands,
+    materials: Res<Materials>,
+    mut segments: ResMut<SnakeSegments>,
+) {
+    segments.0 = vec![
+        commands
+            .spawn(SpriteComponents {
+                material: materials.head_material.clone(),
+                ..Default::default()
+            })
+            .with(SnakeHead {
+                direction: Direction::Up,
+            })
+            .with(SnakeSegment)
+            .with(Position { x: 3, y: 3 })
+            .with(Size::square(0.8))
+            .current_entity()
+            .unwrap(),
+        spawn_segment(
+            &mut commands,
+            &materials.segment_material,
+            Position { x: 3, y: 2 },
+        ),
+    ];
+}
+
+

我们第一个分段是头部,现在我们多加了一个 with(SnakeSegment)。第二个分段来自我们的 spawn_segment 函数。我们现在得到了一条小小的尾巴:

+ +

让尾巴跟着小蛇活动

+
+

点击查看差异

+
+

正如我记得那样,蛇尾没有脱离蛇头,是贪吃蛇游戏中重要的一部分。我们来看看,我们可以怎么修改 snake_movement 函数,来更接近原汁原味的游戏。首先要做的事把 SnakeSegments 资源到 snake_movement 函数上:

+
fn snake_movement(
+    keyboard_input: Res<Input<KeyCode>>,
+    snake_timer: ResMut<SnakeMoveTimer>,
+    segments: ResMut<SnakeSegments>, // <--
+    mut heads: Query<(Entity, &mut SnakeHead)>,
+    mut positions: Query<&mut Position>,
+
+

现在,直接在最前面的 if let 后面,我们加上所有分段的位置(当然,不要忘了蛇头的位置):

+
let segment_positions = segments
+    .0
+    .iter()
+    .map(|e| *positions.get_mut(*e).unwrap())
+    .collect::<Vec<Position>>();
+
+

然后我们要做的是在 if let 的末尾迭代蛇身分段(跳过蛇头,因为我们已经通过用户输入更新了位置),然后让每个分段的位置都变成前一个分段的。例如,第一个蛇身分段设置为当前蛇头(更新前)的位置,第二段的设置为第一段的。

+
segment_positions
+    .iter()
+    .zip(segments.0.iter().skip(1))
+    .for_each(|(pos, segment)| {
+        *positions.get_mut(*segment).unwrap() = *pos;
+    });
+
+

现在我们的游戏看起来应该像这样:

+ +

小蛇成长

+
+

点击查看差异

+
+

小蛇已经饿坏了。我们现在需要家一个系统来让小蛇猎食:

+
fn snake_eating(
+    mut commands: Commands,
+    snake_timer: ResMut<SnakeMoveTimer>,
+    mut growth_events: ResMut<Events<GrowthEvent>>,
+    food_positions: Query<With<Food, (Entity, &Position)>>,
+    head_positions: Query<With<SnakeHead, &Position>>,
+) {
+    if !snake_timer.0.finished {
+        return;
+    }
+    for head_pos in head_positions.iter() {
+        for (ent, food_pos) in food_positions.iter() {
+            if food_pos == head_pos {
+                commands.despawn(ent);
+                growth_events.send(GrowthEvent);
+            }
+        }
+    }
+}
+
+

只是迭代所有的食物位置,来看他们是不是和蛇头共享一个位置,如果是这样,我们就用 despawn 者趁手的函数移除食物,然后触发一个 GrowthEvent。我们来创建这个结构体:

+
struct GrowthEvent;
+
+

使用事件是个新概念。你可以在系统间发送或接受事件,他们可以是任意类型的结构体,使得你可以在事件里包括任何你需要发送的数据。例如,你可能有一个系统发送跳跃事件,然后一个独立的系统来处理他们。在我们的这个案例中,我们需要一个系统来发送成长事件,以及一个成长系统来处理它们。你需要注册事件,就像我们对资源和系统做的那样:

+
.add_event::<GrowthEvent>()
+
+

然后在这里我们也加上 snake_eating 系统:

+
.add_system(snake_eating.system())
+
+

现在小蛇应该能够猎食了。但是小蛇现在就像个黑洞,吃多少也不长大。在思考成长这事时,需要注意我们需要知道最后的分段移动前在哪里,因为那里是新的分段成长的位置。现在我们来创建一个新资源:

+
#[derive(Default)]
+struct LastTailPosition(Option<Position>);
+
+

然后在 app 构建器上:

+
.add_resource(LastTailPosition::default())
+
+

我们也要对 snake_movement 系统做一点小修改,来更新 LastTailPosition 资源。首先先把这个资源加到参数中:

+
fn snake_movement(
+    // ...
+    mut last_tail_position: ResMut<LastTailPosition>, // <--
+    // ...
+
+

然后就是给这个资源分配最后的一个分段的位置。这段代码放在我们迭代过了 segment_positions 之后:

+
last_tail_position.0 = Some(*segment_positions.last().unwrap()); // <--
+
+

之后,小蛇成长的函数就很清晰了:

+
fn snake_growth(
+    mut commands: Commands,
+    last_tail_position: Res<LastTailPosition>,
+    growth_events: Res<Events<GrowthEvent>>,
+    mut segments: ResMut<SnakeSegments>,
+    mut growth_reader: Local<EventReader<GrowthEvent>>,
+    materials: Res<Materials>,
+) {
+    if growth_reader.iter(&growth_events).next().is_some() {
+        segments.0.push(spawn_segment(
+            &mut commands,
+            &materials.segment_material,
+            last_tail_position.0.unwrap(),
+        ));
+    }
+}
+
+

以及追加系统:

+
.add_system(snake_growth.system())
+
+ +

撞墙(或者咬尾巴)

+
+

点击查看差异

+
+

现在我们来增加撞墙和咬尾巴来触发游戏结束(game over)。我们使用一个新事件,就像我们在“小蛇成长小节”中那样:

+
struct GameOverEvent;
+
+

并把它注册到 app 构建器上:

+
.add_event::<GameOverEvent>()
+
+

在我们的 snake_movement 系统中,我们想要访问 “游戏结束” 事件,使得我们能够发送事件:

+
fn snake_movement(
+    // ...
+    mut game_over_events: ResMut<Events<GameOverEvent>>, // <--
+    // ...
+) {
+
+

我们先关注在撞墙事件上面。把这部分代码放到 match &head.direction { 后面:

+
if head_pos.x < 0
+    || head_pos.y < 0
+    || head_pos.x as u32 >= ARENA_WIDTH
+    || head_pos.y as u32 >= ARENA_HEIGHT
+{
+    game_over_events.send(GameOverEvent);
+}
+
+

好了,现在我们的 snake_movement 系统可以发送 “游戏结束” 事件了,我们再来创建一个系统来监听这些事件:

+
fn game_over(
+    mut commands: Commands,
+    mut reader: Local<EventReader<GameOverEvent>>,
+    game_over_events: Res<Events<GameOverEvent>>,
+    materials: Res<Materials>,
+    segments_res: ResMut<SnakeSegments>,
+    food: Query<With<Food, Entity>>,
+    segments: Query<With<SnakeSegment, Entity>>,
+) {
+    if reader.iter(&game_over_events).next().is_some() {
+        for ent in food.iter().chain(segments.iter()) {
+            commands.despawn(ent);
+        }
+        spawn_snake(commands, materials, segments_res);
+    }
+}
+
+

这里有个很酷的点: 我们可以直接使用 spawn_snake 函数,现在它既是一个系统,也是一个辅助函数了。

+

最后一个修改点,就是我们得让小蛇咬到尾巴的时候也会触发 “游戏结束” 事件。在 snake_movement 系统中,在我们检查完边界的部分后添加:

+
if segment_positions.contains(&head_pos) {
+    game_over_events.send(GameOverEvent);
+}
+
+

最后,我们的成果:

+ + +
+ +
+ + + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + diff --git a/tauri-single-instance-bug-dangling.html b/tauri-single-instance-bug-dangling.html new file mode 100644 index 0000000..e8b668f --- /dev/null +++ b/tauri-single-instance-bug-dangling.html @@ -0,0 +1,499 @@ + + + + + + 由 tauri 单例模式 bug “意外修复” 发现的 dangling - huangjj27's Tech blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+
+

由 tauri 单例模式 bug “意外修复” 发现的 dangling

+

TL;DR

+

tauri 单例插件 用于区分单例实例的 productName的过长会导致单例功能失效,博主最初确信 encode_wide 实现有问题,并提交了修复。然而在和社区深入研究问题原因后,发现根本原因是使用 encode_wide 转码传参时造成了 dangling

+

PS: 为方便读者理解,博主花费一天时间重新梳理分析步骤,按照演绎法展示定位 bug 地过程,实现发生的分析过程要比博文的过程更加曲折,对分析理解问题无意义因此略过。

+

所以这个 bug 的现象是怎么样的?

+

正如博主所说,在 有问题版本的插件代码 中,博主最初发现,tauri.conf.json 中的 package.productName 在分别使用五个汉字与六个汉字时,单例模式功能表现出不一致的行为:五个汉字的 productName 单例功能运行正常,而六个汉字的 productName 单例功能失效,于是针对该问题初步进行了测试:

+
+ + + +
测试的 productName单例插件功能是否生效
六个汉字试试x
随便五个字
又来了六个字x
+
+

因为这些汉字测试用例使用 UTF-8 编码,又因为是常用字,因此每个汉字对应 3 bytes,因此假设 productName 在超过 15 bytes、不超过 18 bytes 时会导致功能失效,进一步补充测试用例:

+
+ + +
测试的 productName单例插件功能是否生效
z12345678901234
z123456789012345x
+
+

看来博主运气不错,刚好踩到了边界的测试用例。那么基本可以确定,productName 超过 15 bytes 就会导致单例功能失效。

+

PotatoTooLarge: 传递给 Win32 API 的字符串要用 C string 风格的 \0 结束

+

在最初的讨论过程中,因为我们没有仔细留意插件仓库使用的 encode_wide 是自行做过封装的,因此我们一开始根据 以下代码 进行分析:

+
pub fn init<R: Runtime>(f: Box<SingleInstanceCallback<R>>) -> TauriPlugin<R> {
+    plugin::Builder::new("single-instance")
+        .setup(|app| {
+            let app_name = &app.package_info().name;
+            let class_name = format!("{}-single-instance-class", app_name);
+            let window_name = format!("{}-single-instance-window", app_name);
+
+            let hmutex = unsafe {
+                CreateMutexW(
+                    std::ptr::null(),
+                    true.into(),
+                    encode_wide("tauri-plugin-single-instance-mutex").as_ptr(),
+                )
+            };
+
+            if unsafe { GetLastError() } == ERROR_ALREADY_EXISTS {
+                unsafe {
+                    let hwnd = FindWindowW(
+                        encode_wide(&class_name).as_ptr(),
+                        encode_wide(&window_name).as_ptr(),
+                    );
+
+                    // omitted
+                }
+
+                // omitted
+            }
+
+            // omitted
+        })
+        .on_event(|app, event| {
+            // omitted
+        })
+        .build()
+}
+
+

有 Windows 编程经验的 @PotatoTooLarge 指出,encode_wide(来自 std 的 Window扩展)并不会补充 \0 结束符:

+
+

Re-encodes an OsStr as a wide character sequence, i.e., potentially ill-formed UTF-16.

+

This is lossless: calling OsStringExt::from_wide and then encode_wide on the result will yield the original code units. Note that the encoding does not add a final null terminator.

+
+

于是博主听取建议,把所有会传递到 encode_wide 函数的字符串都添加了 \0,形成了 一版修复:

+
pub fn init<R: Runtime>(f: Box<SingleInstanceCallback<R>>) -> TauriPlugin<R> {
+    plugin::Builder::new("single-instance")
+        .setup(|app| {
+            let app_name = &app.package_info().name;
+            // let class_name = format!("{}-single-instance-class", app_name);
+            let class_name = format!("{}-single-instance-class\0", app_name);
+            // let window_name = format!("{}-single-instance-window", app_name);
+            let window_name = format!("{}-single-instance-window\0", app_name);
+
+            let hmutex = unsafe {
+                CreateMutexW(
+                    std::ptr::null(),
+                    true.into(),
+                    // encode_wide("tauri-plugin-single-instance-mutex").as_ptr(),
+                    encode_wide("tauri-plugin-single-instance-mutex\0").as_ptr(),
+                )
+            };
+
+            if unsafe { GetLastError() } == ERROR_ALREADY_EXISTS {
+                unsafe {
+                    let hwnd = FindWindowW(
+                        encode_wide(&class_name).as_ptr(),
+                        encode_wide(&window_name).as_ptr(),
+                    );
+
+                    // omitted
+                }
+
+                // omitted
+            }
+
+            // omitted
+        })
+        .on_event(|app, event| {
+            // omitted
+        })
+        .build()
+}
+
+

然后使用修复前会引起单例功能失效的 z123456789012345 作为测试用例,验证单例功能可用了,证明该修改可以修复单例功能失效的问题。

+

但它并不是真的修复

+

在博主提交了修复后,插件仓库作者提醒,前文所述的代码使用的 encode_wide封装拼接了 \0 后再传递参数的:

+
pub fn encode_wide(string: impl AsRef<std::ffi::OsStr>) -> Vec<u16> {
+    std::os::windows::prelude::OsStrExt::encode_wide(string.as_ref())
+        .chain(std::iter::once(0))
+        .collect()
+}
+
+

这意味着,并不是 \0 导致问题的失效,因为该函数在 windows 环境下执行是能够补足 \0 的:

+
fn encode_wide(string: impl AsRef<std::ffi::OsStr>) -> Vec<u16> {
+    std::os::windows::prelude::OsStrExt::encode_wide(string.as_ref())
+        .chain(std::iter::once(0))
+        .collect()
+}
+
+fn main() {
+    let product_name = "z123456789012345";
+
+    // output: [122, 49, 50, 51, 52, 53, 54, 55, 56, 57, 48, 49, 50, 51, 52, 53, 0]
+    //                                                                           ^
+    //                              null concated here so it's null-terminated --|
+    println!("{:?}", encode_wide(product_name));
+}
+
+

那么失效的过程发生了什么?

+

为了分析问题详细过程,我将插件仓库代码切换到了 问题代码版本

+
git checkout 16e5e9eb59da9ceca3dcf09c81120b37fe108a03
+
+

然后添加了一些 dbg 宏:

+
pub fn init<R: Runtime>(f: Box<SingleInstanceCallback<R>>) -> TauriPlugin<R> {
+    plugin::Builder::new("single-instance")
+        .setup(|app| {
+            let app_name = &app.package_info().name;
+            let class_name = format!("{}-single-instance-class", app_name);
+            let window_name = format!("{}-single-instance-window", app_name);
+
+            let hmutex = unsafe {
+                CreateMutexW(
+                    std::ptr::null(),
+                    true.into(),
+                    encode_wide("tauri-plugin-single-instance-mutex").as_ptr(),
+                )
+            };
+            dbg!(hmutex);  // windows.rs:43 debug here!
+
+            if unsafe { GetLastError() } == ERROR_ALREADY_EXISTS {
+                unsafe {
+                    let hwnd = FindWindowW(
+                        encode_wide(&class_name).as_ptr(),
+                        encode_wide(&window_name).as_ptr(),
+                    );
+                    dbg!(hwnd);  // windows.rs:51 debug here!
+
+                    // omitted
+                }
+            } else {
+                app.manage(MutexHandle(hmutex));
+
+                let hwnd = create_event_target_window::<R>(&class_name, &window_name);
+                dbg!(hwnd);  // windows.rs:76 debug here!
+
+                // omitted
+            }
+
+            Ok(())
+        })
+        .on_event(|app, event| {
+            // omitted
+        })
+        .build()
+}
+
+

然后,将代码仓库 examples\emit-event\src-tauri\tauri.conf.json 分别改成 z12345678901234z123456789012345,然后执行:

+
# process1
+> cd examples\emit-event
+examples\emit-event> cargo tauri build --debug
+examples\emit-event> src-tauri\target\debug\z12345678901234.exe
+[tauri-plugin-single-instance\src\platform_impl\windows.rs:43] hmutex = 548
+[tauri-plugin-single-instance\src\platform_impl\windows.rs:76] hwnd = 40113446
+
+# process2
+> examples\emit-event\src-tauri\target\debug\z12345678901234.exe
+[tauri-plugin-single-instance\src\platform_impl\windows.rs:43] hmutex = 548
+[tauri-plugin-single-instance\src\platform_impl\windows.rs:51] hwnd = 40113446
+
+
# process1
+> cd examples\emit-event
+examples\emit-event> cargo tauri build --debug
+examples\emit-event> src-tauri\target\debug\z123456789012345.exe
+[tauri-plugin-single-instance\src\platform_impl\windows.rs:43] hmutex = 552
+[tauri-plugin-single-instance\src\platform_impl\windows.rs:76] hwnd = 0
+
+# process2
+> examples\emit-event\src-tauri\target\debug\z123456789012345.exe
+[tauri-plugin-single-instance\src\platform_impl\windows.rs:43] hmutex = 548
+[tauri-plugin-single-instance\src\platform_impl\windows.rs:51] hwnd = 0
+
+# process3
+> examples\emit-event\src-tauri\target\debug\z123456789012345.exe
+[tauri-plugin-single-instance\src\platform_impl\windows.rs:43] hmutex = 548
+[tauri-plugin-single-instance\src\platform_impl\windows.rs:51] hwnd = 0
+
+

由上面的结果我们可以知道:在用例 z12345678901234,我们在创建实实例时返回了有效的 hwnd 值,并且在检查 hwnd 时确认已经创建窗口;而在用例 z123456789012345,我们创建窗口的函数 create_event_target_window 返回的 hwnd 是无效的!所以导致问题的代码,应该在 create_event_target_window 的逻辑中!再次添加 dbg ,重新编译后继续 debug:

+
fn create_event_target_window<R: Runtime>(class_name: &str, window_name: &str) -> HWND {
+    unsafe {
+        let class = WNDCLASSEXW {
+            cbSize: std::mem::size_of::<WNDCLASSEXW>() as u32,
+            style: 0,
+            lpfnWndProc: Some(single_instance_window_proc::<R>),
+            cbClsExtra: 0,
+            cbWndExtra: 0,
+            hInstance: GetModuleHandleW(std::ptr::null()),
+            hIcon: 0,
+            hCursor: 0,
+            hbrBackground: 0,
+            lpszMenuName: std::ptr::null(),
+            lpszClassName: encode_wide(&class_name).as_ptr(),
+            hIconSm: 0,
+        };
+        dbg!(class.lpszClassName);  // windows.rs:153 debug here
+        dbg!(*class.lpszClassName);  // windows.rs:154 debug here
+
+        RegisterClassExW(&class);
+
+        let hwnd = CreateWindowExW(
+            WS_EX_NOACTIVATE
+            | WS_EX_TRANSPARENT
+            | WS_EX_LAYERED
+            // WS_EX_TOOLWINDOW prevents this window from ever showing up in the taskbar, which
+            // we want to avoid. If you remove this style, this window won't show up in the
+            // taskbar *initially*, but it can show up at some later point. This can sometimes
+            // happen on its own after several hours have passed, although this has proven
+            // difficult to reproduce. Alternatively, it can be manually triggered by killing
+            // `explorer.exe` and then starting the process back up.
+            // It is unclear why the bug is triggered by waiting for several hours.
+            | WS_EX_TOOLWINDOW,
+            dbg!(encode_wide(&class_name).as_ptr()),  // windows.rs:170 debug here
+            dbg!(encode_wide(&window_name).as_ptr()),  // windows.rs:171 debug here
+            WS_OVERLAPPED,
+            0,
+            0,
+            0,
+            0,
+            0,
+            0,
+            GetModuleHandleW(std::ptr::null()),
+            std::ptr::null(),
+        );
+        SetWindowLongPtrW(
+            hwnd,
+            GWL_STYLE,
+            // The window technically has to be visible to receive WM_PAINT messages (which are used
+            // for delivering events during resizes), but it isn't displayed to the user because of
+            // the LAYERED style.
+            (WS_VISIBLE | WS_POPUP) as isize,
+        );
+        hwnd
+    }
+}
+
+

z12345678901234:

+
examples\emit-event> src-tauri\target\debug\z12345678901234.exe
+[tauri-plugin-single-instance\src\platform_impl\windows.rs:43] hmutex = 556
+[tauri-plugin-single-instance\src\platform_impl\windows.rs:153] class.lpszClassName = 0x0000021d099eddc0
+[tauri-plugin-single-instance\src\platform_impl\windows.rs:154] *class.lpszClassName = 122
+[tauri-plugin-single-instance\src\platform_impl\windows.rs:170] encode_wide(&class_name).as_ptr() = 0x0000021d099ee180
+[tauri-plugin-single-instance\src\platform_impl\windows.rs:171] encode_wide(&window_name).as_ptr() = 0x0000021d099dfe20
+[tauri-plugin-single-instance\src\platform_impl\windows.rs:76] hwnd = 28841288
+
+

z123456789012345:

+
examples\emit-event> src-tauri\target\debug\z123456789012345.exe
+[tauri-plugin-single-instance\src\platform_impl\windows.rs:43] hmutex = 548
+[tauri-plugin-single-instance\src\platform_impl\windows.rs:153] class.lpszClassName = 0x0000017259ca6be0
+[tauri-plugin-single-instance\src\platform_impl\windows.rs:154] *class.lpszClassName = 43920
+[tauri-plugin-single-instance\src\platform_impl\windows.rs:170] encode_wide(&class_name).as_ptr() = 0x0000017259cc6b30
+[tauri-plugin-single-instance\src\platform_impl\windows.rs:171] encode_wide(&window_name).as_ptr() = 0x0000017259cc6970
+[tauri-plugin-single-instance\src\platform_impl\windows.rs:76] hwnd = 0
+
+

对比我们之前 encode_wide 函数返回的结果class_name 开头的字符应该是 ASCII 字符 z(ASCII 码 122),因此通过 encode_wide(&class_name).as_ptr() 传参的 class.lpszClassName,应当指向值为 "z123456789012345-single-instance-class" 的字符串,这在用例 z12345678901234 中行为符合预期;但在 z123456789012345 的用例中,class.lpszClassName 指向的却发生了变化(*class.lpszClassName = 43920),反推可以得知, encode_wide(&class_name).as_ptr() 并没有成功地把指针传递给 class.lpszClassName

+

一语惊醒梦中人:悬垂指针(dangling)!

+

@Berrysoft 指出, encode_wide(&class_name).as_ptr() 这种写法由于直接对临时变量直接取指针,而临时变量 encode_wide(&class_name) 会在执行完之后被马上释放结束生命周期,因此指向该临时变量的指针也会变成悬垂指针!临时变量的这一行为在 reference 中有说明:

+
+

When using a value expression in most place expression contexts, a temporary unnamed memory location is created and initialized to that value. The expression evaluates to that location instead, except if promoted to a static. The drop scope of the temporary is usually the end of the enclosing statement.

+
+

而解决该问题,只需要把提升变量的 lifetime,把要用到的变量提取出来,使其 lifetime 可以覆盖要用到的函数而不至于在语句执行完之后马上被回收。于是有了解决问题的 PR

+

那为什么在 format 的时候手动添加 \0 后,问题“修复”了呢?

+

这个问题依然悬而未决。有 TG 群友提出,可能是由于堆栈被破坏 “碰巧” 又指向了正确的字符串位置,而 format 后的变量又是 'static 的,因此能达到“修复”的效果,然而这依然是基于 bug/undefined behavior 的修复方案,因此仍然不可靠。后续原因排查出来后会更新博客~

+

教训与经验

+
    +
  1. 实际上的排查过程,是分析过一次 create_event_target_window 的,然而当时由于需求紧急而找到了临时绕开的实现方案(把 productName 砍短),因此搁置了,也没有留下相关的排查记录文档,以致于后续在需求变更而变得必须排查清楚该问题时,走向了排查 encode_wide 的错误方向,虽然有了“修复”方案,但该方案仍然不可靠,因此可以视作浪费了实践。 形成记录首先方便的是以后的自己。
  2. +
  3. 凡是 unsafe 多查几遍。像本文涉及到的悬垂指针问题,在 safe rust 中因为 lifetime 不够长而会阻止编译,而 unsafe 块中使用裸指针是不会被编译器检查的,因此相关操作都要相当慎重。
  4. +
  5. 多借助社区的力量。比起一个人钻牛角尖,多与社区讨论才容易跳出原本的死胡同,从而理解意识到原来思路的局限性。
  6. +
+ +
+ +
+ + + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + diff --git a/tomorrow-night.css b/tomorrow-night.css new file mode 100644 index 0000000..5b4aca7 --- /dev/null +++ b/tomorrow-night.css @@ -0,0 +1,102 @@ +/* Tomorrow Night Theme */ +/* http://jmblog.github.com/color-themes-for-google-code-highlightjs */ +/* Original theme - https://github.com/chriskempson/tomorrow-theme */ +/* http://jmblog.github.com/color-themes-for-google-code-highlightjs */ + +/* Tomorrow Comment */ +.hljs-comment { + color: #969896; +} + +/* Tomorrow Red */ +.hljs-variable, +.hljs-attribute, +.hljs-tag, +.hljs-regexp, +.ruby .hljs-constant, +.xml .hljs-tag .hljs-title, +.xml .hljs-pi, +.xml .hljs-doctype, +.html .hljs-doctype, +.css .hljs-id, +.css .hljs-class, +.css .hljs-pseudo { + color: #cc6666; +} + +/* Tomorrow Orange */ +.hljs-number, +.hljs-preprocessor, +.hljs-pragma, +.hljs-built_in, +.hljs-literal, +.hljs-params, +.hljs-constant { + color: #de935f; +} + +/* Tomorrow Yellow */ +.ruby .hljs-class .hljs-title, +.css .hljs-rule .hljs-attribute { + color: #f0c674; +} + +/* Tomorrow Green */ +.hljs-string, +.hljs-value, +.hljs-inheritance, +.hljs-header, +.hljs-name, +.ruby .hljs-symbol, +.xml .hljs-cdata { + color: #b5bd68; +} + +/* Tomorrow Aqua */ +.hljs-title, +.css .hljs-hexcolor { + color: #8abeb7; +} + +/* Tomorrow Blue */ +.hljs-function, +.python .hljs-decorator, +.python .hljs-title, +.ruby .hljs-function .hljs-title, +.ruby .hljs-title .hljs-keyword, +.perl .hljs-sub, +.javascript .hljs-title, +.coffeescript .hljs-title { + color: #81a2be; +} + +/* Tomorrow Purple */ +.hljs-keyword, +.javascript .hljs-function { + color: #b294bb; +} + +.hljs { + display: block; + overflow-x: auto; + background: #1d1f21; + color: #c5c8c6; +} + +.coffeescript .javascript, +.javascript .xml, +.tex .hljs-formula, +.xml .javascript, +.xml .vbscript, +.xml .css, +.xml .hljs-cdata { + opacity: 0.5; +} + +.hljs-addition { + color: #718c00; +} + +.hljs-deletion { + color: #c82829; +} diff --git a/wasi/intro.html b/wasi/intro.html new file mode 100644 index 0000000..a9f48cc --- /dev/null +++ b/wasi/intro.html @@ -0,0 +1,210 @@ + + + + + + WASI探索 - huangjj27's Tech blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+
+

WASM(Web Assembly)尽管是为了提高网页中性能敏感模块表现而提出的字节码标准, 但是WASM却不仅能用在浏览器(broswer)中, 也可以用在其他环境中. 在这些环境中, 我们则需要支持WASI(WebAssembly System Interface, WASM系统接口)的runtime来执行我们编译运行的wasm模块.

+ +
+ +
+ + + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + diff --git a/wasi/wasi_and_wasmtime.html b/wasi/wasi_and_wasmtime.html new file mode 100644 index 0000000..998cda5 --- /dev/null +++ b/wasi/wasi_and_wasmtime.html @@ -0,0 +1,246 @@ + + + + + + WASI简介与Wasmtime配置 - huangjj27's Tech blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+
+

WASI探索(一) -- WASI简介与Wasmtime配置

+

什么是WASI?

+

WASI1是一个新的API体系, 由Wasmtime项目设计, 目的是为WASM设计一套引擎无关(engine-indepent), 面向非Web系统(non-Web system-oriented)的API标准. 目前, WASI核心API(WASI Core)在做覆盖文件, 网络等等模块的API, 但这些实现都是刚刚开始实现, 离实用还是有很长路要走.

+

关于WASM runtime

+

在了解了WASI之后, 博主最后选定两个WASM运行时进行探索: WASMER 与 Wasmtime. 这两款运行时都号称开始支持了WASI标准, 但博主使用rust-wasi-tutorial对两款运行时进行试验后, 发现WASMER对于文件读取还是有些问题, 而Wasmtime则是通过了规格测试(基于specs testsuite), 因此本文接下来着重于Wasmtime的配置介绍.

+

Wasmtime与rust环境配置

+

由于目前Wasmtime与WASMER均只支持Unix-like环境, 接下来楼主将演示如何在WSL(Ubuntu 18.04)下配置Wasmtime. 而在目前比较方便生成wasm的编程语言中, 博主选择使用自带wasi目标的rust编程语言, 可以"零代价"配置wasm相关工具链.

+

配置rust

+
    +
  1. 下载并安装rustup: curl https://sh.rustup.rs -sSf | sh, 安装时使用默认 stable-x86_64-unknown-linux-gnu工具链, 后面我们还会自行添加用于编译wasm的nightly工具链.
  2. +
  3. 为cargo配置ustc反代, 提高crates(rust库)下载速度2
  4. +
  5. 安装rustfmt: rustup component add rustfmt --toolchain stable-x86_64-unknown-linux-gnu. Wasmtime的test脚本需要用到该组件.
  6. +
  7. 安装rust nightly工具链: rustup toolchain add nightly-x86_64-unknown-linux-gnu. 当前rust的WASI目标还在开发中, 尚未稳定3.
  8. +
  9. 安装rust WASI目标: rustup target add wasm32-unknown-wasi3.
  10. +
+

配置Wasmtime

+
    +
  1. 安装cmake与clang: sudo apt install cmake clang, 用于编译Wasmtime. Wasmtime目前尚未有正式发布版本, 故需要我们自行编译.
  2. +
  3. 拷贝Wasmtime源码: git clone --recursive git@github.com:CraneStation/wasmtime.git ~/wasmtime.
  4. +
  5. 切换到Wasm源码目录: cd ~/wasmtime
  6. +
  7. 执行测试脚本: ./scripts/test-all.sh. 当脚本执行完毕并通过测试后, 说明wasmtime已经正常编译并且能在当前WSL环境下正常工作, 可以使用生成的wasmtime可执行文件.
  8. +
  9. 将生成的wasmtime拷贝到/usr/bin目录中: cp ~/wasmtime/target/release/wasmtime /usr/bin, 以便在整个WSL环境中任意目录执行wasmtime. wasmtime是个单文件(stand alone)运行时.
  10. +
  11. 执行wasmtime --help命令, 确认wasmtime成功安装.
  12. +
+

试验

+

GitHub上面已经有了比较简单的试验, 大家按照上面的说明 +去试验即可. 下一篇文章, 博主将会把猜数字编译成WASI目标并执行, 同时会尝试把一些常用的库尝试编译, 来探究 +当前社区对WASI支持的程度.

+ + +
3 +

截至2020年1月19日,WASI目标已经稳定并重命名为wasm32-wasi

+
+ +
+ +
+ + + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + diff --git a/wasi/wasi_guess.html b/wasi/wasi_guess.html new file mode 100644 index 0000000..8a04d98 --- /dev/null +++ b/wasi/wasi_guess.html @@ -0,0 +1,649 @@ + + + + + + WASI版猜数字 - huangjj27's Tech blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+
+

WASI探索(二) -- WASI版猜数字

+
+

猜数字作为入门Rust时第一次编写并具有实际功能的程序,适合让读者快速掌握rust的基本概念。同时,为了让程序更加有趣,博主在原本的猜数字程序上添加了日志和从运行时参数传递游戏难度的功能。此外,由于博主偏好改变,本文还会涉及到另外一款WASI运行时Wasmer,以及他们为了丰富WASI生态而推出的wasm包管理器wapm。

+
+

阅读须知

+

学习外部资料更有助于读者了解相关生态,因此本文将不赘述:

+ +

而阅读本文,你将了解:

+
    +
  • 如何用日志debug的一些原则
  • +
  • 一个简单的配置文件的设计
  • +
  • 读者对Wasmer的一些浅薄看法
  • +
+

原版猜数字

+

我们从官方书拷贝了一份猜数字程序:

+
// main.rs
+use std::io;
+use std::cmp::Ordering;
+use rand::Rng;
+
+fn main() {
+    println!("Guess the number!");
+
+    let secret_number = rand::thread_rng().gen_range(1, 101);
+
+    loop {
+        println!("Please input your guess.");
+
+        let mut guess = String::new();
+
+        io::stdin().read_line(&mut guess)
+            .expect("Failed to read line");
+
+        let guess: u32 = match guess.trim().parse() {
+            Ok(num) => num,
+            Err(_) => continue,
+        };
+
+        println!("You guessed: {}", guess);
+
+        match guess.cmp(&secret_number) {
+            Ordering::Less => println!("Too small!"),
+            Ordering::Greater => println!("Too big!"),
+            Ordering::Equal => {
+                println!("You win!");
+                break;
+            }
+        }
+    }
+}
+
+
# Cargo.toml
+[package]
+name = "guessing_game"
+version = "0.1.0"
+authors = ["Your Name <you@example.com>"]
+edition = "2018"
+
+[dependencies]
+rand = "0.3.14"
+
+

一次游戏只猜一个数

+

我们可以看到,这个程序每次运行,只能猜一个数字,如果要继续玩就只能重新启动。但是博主想让这个游戏,能在一次运行时 +可以生成不同难度关卡,因此首先我们将“猜一个数字”逻辑抽取成可复用函数

+
#![allow(unused)]
+fn main() {
+// main.rs
+fn guess_a_number() {
+    println!("Guess the number!");
+
+    let secret_number = rand::thread_rng().gen_range(1, 101);
+
+    loop {
+        println!("Please input your guess.");
+
+        let mut guess_str = String::new();
+
+        io::stdin().read_line(&mut guess_str)
+            .expect("Failed to read line");
+
+        let guess: u32 = match guess_str.trim().parse() {
+            Ok(num) => num,
+            Err(_) => continue,
+        };
+
+        println!("You guessed: {}", guess);
+
+        match guess.cmp(&secret_number) {
+            Ordering::Less => println!("Too small!"),
+            Ordering::Greater => println!("Too big!"),
+            Ordering::Equal => {
+                println!("You win!");
+                break;
+            }
+        }
+    }
+}
+}
+
+

再将配置文件修改一下:

+
# guess_wasi/Cargo.toml
+[package]
+name = "guess_wasi"
+version = "0.1.0"
+authors = ["huangjj27 <huangjj.27@qq.com>"]
+edition = "2018"
+
+[dependencies]
+rand = "0.7"
+
+

此外,猜数字游戏的难度取决于随机生成数字的范围, 为了生成不同的难度关卡,我们需要guess_a_number接受一组控制 +生成数字范围的参数:

+
#![allow(unused)]
+fn main() {
+// main.rs
+/// 生成熟悉范围的下界(lower bound,lb)与上界(higher bound,hb)在主函数中读取配置文件得到
+fn guess_a_number((lb, hb): (u32, u32)) {
+    println!("Guess the number!");
+
+    let secret_number = rand::thread_rng().gen_range(lb, hb + 1);
+
+    // ...
+}
+}
+
+

这里在传入参数时,直接解构元组, 这样后面就可以直接使用传入的上界与下界来控制生成数范围

+

然后,博主发现, 原版猜数字如果解析数字错误的话会直接跳过,博主觉得这里应该至少提醒一下用户输入错误了:

+
#![allow(unused)]
+fn main() {
+// main.rs
+fn guess_a_number((lb, hb): (u32, u32)) {
+    // ...
+        let guess: u32 = match guess_str.trim().parse() {
+            Ok(num) => num,
+            Err(_) => {
+                println!("Input not a number! please input a number");
+                continue;
+            },
+        };
+    // ...
+}
+}
+
+

加上log追踪生成的数据情况

+

使用log去追踪数据与可能产生bug的代码有以下好处:

+
    +
  • 了解运行时所关注的数据情况, 方便定位bug
  • +
  • 清晰地知道实际运行流程是否如期望那样执行
  • +
  • 即便使用release版目标, 仍然可以获得需要的分析信息
  • +
  • 区分产生信息的层级,以便将精力集中在优先需要处理的信息中
  • +
+

回到猜数字游戏上,博主想要知道每一次游戏中知道生成的secret_number是多少, 并且根据运行时输入的日志层级的参数 +决定是否显示这个数字,需求相对简单,因此使用rust生态中比较常用的log crateenv_log crate。在Cargo.toml中加入两个新依赖:

+
# guess_wasi/Cargo.toml
+
+# ...
+
+[dependencies]
+rand = "0.7"
+
+# 总是使用最新的log与env_log
+log = "*"
+env_logger = "*"
+
+

加入追踪日志代码:

+
// main.rs
+use log::{trace, debug};
+
+fn main() {
+    // 别忘了初始化日志生成器, 才能获取日志!
+    env_log::init();
+    guess_a_number((1, 100));
+}
+
+fn guess_a_number((lb, hb): (u32, u32)) {
+    println!("Guess the number!");
+
+    let secret_number = rand::thread_rng().gen_range(lb, hb + 1);
+    trace!("secret number: {}", secret);
+
+    loop {
+        println!("Please input your guess.");
+
+        let mut guess_str = String::new();
+        io::stdin().read_line(&mut guess_str)
+            .expect("Failed to read line");
+        debug!("scaned string: {:?}", guess_str);
+
+        // ...
+    }
+}
+
+

向高难度挑战!

+

现在我们来到最后了一个需求:通过运行时参数来给每次游戏输入多个游戏难度,这个难度由随机数生成的范围决定-- 随机数 +生成的范围越大,一次猜中这个数的概率就越小。为方便地写出输入参数的命令,我们需要引入structopt库(crate), +最后获得类似--levels=10 100 1000这样的参数输入方式, 参数中每个数字表示每次生成随机数的生成范围上界。

+

配置文件追加:

+
# guess_wasi/Cargo.toml
+
+# ...
+
+[dependencies]
+rand = "0.7"
+
+# 总是使用最新的log与env_log
+log = "*"
+env_logger = "*"
+
+structopt = "*"
+
+

编写参数代码。

+
// main.rs
+
+// ...
+use structopt::StructOpt;
+
+// 定义参数只需要把他们的名字和类型写在一个参数结构体中即可!
+#[derive(StructOpt)]
+#[structopt(name="guess_wasi")]
+struct Opt {
+    #[structopt(long="levels")]
+    levels: Vec<u32>,
+}
+
+fn main() {
+    env_logger::init();
+
+    // 获取并访问levels参数, 只需要访问参数结构体的对应成员即可, 细节处理可以方便地交给库执行!
+    let opt = Opt::from_args();
+    for &lv in &opt.levels {
+        println!("given number range 0~{}", lv);
+        guess_a_number((0, lv));
+    }
+}
+
+

完整代码

+
[package]
+name = "guess_wasi"
+version = "0.1.0"
+authors = ["huangjj27 <huangjj.27@qq.com>"]
+edition = "2018"
+
+[dependencies]
+rand = "0.7"
+
+# 总是使用最新的log与env_log
+log = "*"
+env_logger = "*"
+
+structopt = "*"
+
+
// guess_wasi/main.rs
+use std::io;
+use std::cmp::Ordering;
+use rand::Rng;
+use log::{debug, trace};
+
+use structopt::StructOpt;
+
+// 定义参数只需要把他们的名字和类型写在一个参数结构体中即可!
+#[derive(StructOpt)]
+#[structopt(name="guess_wasi")]
+struct Opt {
+    #[structopt(long="levels")]
+    levels: Vec<u32>,
+}
+
+fn main() {
+    env_logger::init();
+
+    // 获取并访问levels参数, 只需要访问参数结构体的对应成员即可, 细节处理可以方便地交给库执行!
+    let opt = Opt::from_args();
+    for &lv in &opt.levels {
+        println!("given number range 0~{}", lv);
+        guess_a_number((0, lv));
+    }
+}
+
+// 一场游戏有多个难度,我们每个难度只猜一个数字,然后变难
+fn guess_a_number((lb, hb): (u32, u32)) {
+    let secret = rand::thread_rng().gen_range(lb, hb + 1);
+    trace!("secret number: {}", secret);
+
+    loop {
+        println!("Please input your guess.");
+
+        let mut guess_str = String::new();
+        io::stdin().read_line(&mut guess_str)
+            .expect("Failed to read line");
+        debug!("scaned string: {:?}", guess_str);
+
+        let guess: u32 = match guess_str.trim().parse() {
+            Ok(num) => num,
+            Err(_) => {
+                println!("Input not a number! please input a number");
+                continue;
+            },
+        };
+
+        println!("You guessed: {}", guess);
+
+        match guess.cmp(&secret) {
+            Ordering::Less => println!("too small!"),
+            Ordering::Greater => println!("too big!"),
+            Ordering::Equal => {
+                println!("You get it!");
+                break;
+            }
+        }
+    }
+}
+
+

读到这里,读者可以发现前文根本没涉及到WASI,甚至没有涉及WASM。这因为WASI作为应用与运行时交互的接口,被rust编译器封装成为编译目标,读者只需要编译到对应目标即可让自己的程序在对应平台上运行. 这是Rust编程语言现代化与工程学的体现: +一般应用研发工程师可以通过使用已经适配所需平台的底层库(这些底层库通常已经针对所有支持平台做了最优化适配),就能让自己的应用支持对应的平台而无需重新编写针对某平台的特化版本源码!

+

是时候编译成WASI目标了

+

我们还需要添加对应的编译目标:

+
rustup target add wasm32-wasi
+
+

编译到wasm32-wasi目标上:

+
$ cargo build --target=wasm32-wasi --release
+   Compiling proc-macro2 v1.0.18
+   Compiling version_check v0.9.2
+   Compiling unicode-xid v0.2.0
+   Compiling syn v1.0.30
+   Compiling cfg-if v0.1.10
+   Compiling memchr v2.3.3
+   Compiling getrandom v0.1.14
+   Compiling wasi v0.9.0+wasi-snapshot-preview1
+   Compiling lazy_static v1.4.0
+   Compiling bitflags v1.2.1
+   Compiling atty v0.2.14
+   Compiling unicode-width v0.1.7
+   Compiling unicode-segmentation v1.6.0
+   Compiling log v0.4.8
+   Compiling quick-error v1.2.3
+   Compiling ansi_term v0.11.0
+   Compiling ppv-lite86 v0.2.8
+   Compiling regex-syntax v0.6.18
+   Compiling strsim v0.8.0
+   Compiling vec_map v0.8.2
+   Compiling termcolor v1.1.0
+   Compiling thread_local v1.0.1
+   Compiling textwrap v0.11.0
+   Compiling proc-macro-error-attr v1.0.2
+   Compiling proc-macro-error v1.0.2
+   Compiling humantime v1.3.0
+   Compiling heck v0.3.1
+   Compiling quote v1.0.7
+   Compiling rand_core v0.5.1
+   Compiling clap v2.33.1
+   Compiling regex v1.3.9
+   Compiling rand_chacha v0.2.2
+   Compiling env_logger v0.7.1
+   Compiling syn-mid v0.5.0
+   Compiling rand v0.7.3
+   Compiling structopt-derive v0.4.7
+   Compiling structopt v0.3.14
+   Compiling guess_wasi v0.1.0 (C:\Users\huangjj27\Documents\codes\huangjj27.github.io\code\guess_wasi)
+    Finished release [optimized] target(s) in 4m 58s
+
+

现在,我们来运行一下程序吧:

+
$ wasmer --version
+wasmer 0.13.1
+$ wasmer run .\target\wasm32-wasi\release\guess.wasm --env RUST_LOG=trace -- --levels 10 100 1000
+given number range 0~10
+[2020-06-09T14:55:58Z TRACE guess] secret number: 10
+Please input your guess.
+5
+[2020-06-09T14:56:02Z DEBUG guess] scaned string: "5\r\n"
+You guessed: 5
+too small!
+Please input your guess.
+8
+[2020-06-09T14:56:04Z DEBUG guess] scaned string: "8\r\n"
+You guessed: 8
+too small!
+Please input your guess.
+9
+[2020-06-09T14:56:07Z DEBUG guess] scaned string: "9\r\n"
+You guessed: 9
+too small!
+Please input your guess.
+10
+[2020-06-09T14:56:09Z DEBUG guess] scaned string: "10\r\n"
+You guessed: 10
+You get it!
+given number range 0~100
+[2020-06-09T14:56:09Z TRACE guess] secret number: 60
+Please input your guess.
+60
+[2020-06-09T14:56:25Z DEBUG guess] scaned string: "60\r\n"
+You guessed: 60
+You get it!
+given number range 0~1000
+[2020-06-09T14:56:25Z TRACE guess] secret number: 715
+Please input your guess.
+300
+[2020-06-09T14:56:32Z DEBUG guess] scaned string: "300\r\n"
+You guessed: 300
+too small!
+Please input your guess.
+720
+[2020-06-09T14:56:38Z DEBUG guess] scaned string: "720\r\n"
+You guessed: 720
+too big!
+Please input your guess.
+716
+[2020-06-09T14:56:41Z DEBUG guess] scaned string: "716\r\n"
+You guessed: 716
+too big!
+Please input your guess.
+714
+[2020-06-09T14:56:46Z DEBUG guess] scaned string: "714\r\n"
+You guessed: 714
+too small!
+Please input your guess.
+715
+[2020-06-09T14:56:48Z DEBUG guess] scaned string: "715\r\n"
+You guessed: 715
+You get it!
+$
+
+

调试后,确认我们的程序可以正常执行了, 去掉--env RUST_LOG=trace参数,享受自己制作的这个小游戏吧!

+ +

Wasmer与Wapm

+

Wasmer可以是说在WASI生态中响应速度仅次于Mozilla的组织,他们号称打造了 +一款可以让代码“一次构建,处处运行”(Build Once, Run Anywhere.)的运行时环境,该环境可以运行ECMAScripten标准与 +WASI标准的wasm栈机码。并且方便为wasm代码分发,该组织开发了类似于nodejs生态中npm的包管理工具wapm,这样用户就可以 +很轻松地发布自己的程序,以及利用他人的程序了--这促进了WASM生态的发展,同时作为生态底层的领导者,Wasmer也将拥有 +更多发言权。

+

作为边缘人士(稍微知道WASM生态但没很深入了解),博主看到这项目背后的布局很像上世纪Sun公司的Java和JVM(尽管WASM并不是Wasmer的发明,但这样反而不必为WASM这样可以作为主流编程语言编译目标工具投入过多精力宣传,可以集中精力去优化wasmer与wapm;同时因为wasmer是使用MIT协议授权,不会产生类似OracleJDK专利权所属的问题,相信随着生态的进一步发展,在虚拟机运行时领域会逐步替代JVM成为主流,届时将解放程序员更多生产力 -- 不必要求掌握Java而是通过自己熟悉的编程语言(c/c++/rust/python/...)通过统一的标准相互调用(进一步微型化的微服务)。

+

而这个在服务器/PC桌面应用占主导地位的标准,就是WASI。

+ +
+ +
+ + + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + +