This commit is contained in:
Cristian Carlesso @kentaromiura 2020-05-03 23:12:49 +09:00
parent cf3d4046a3
commit 6b4aabb6e3
23 changed files with 108608 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
node_modules/
dialog.r
build/

17
.vscode/c_cpp_properties.json vendored Normal file
View File

@ -0,0 +1,17 @@
{
"configurations": [
{
"name": "Linux",
"includePath": [
"${workspaceFolder}/**",
"../R68/toolchain/m68k-apple-macos/include"
],
"defines": [],
"compilerPath": "/usr/bin/clang",
"cStandard": "c11",
"cppStandard": "c++17",
"intelliSenseMode": "clang-x64"
}
],
"version": 4
}

7
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,7 @@
{
"files.associations": {
"quickdraw.h": "c",
"cstring": "cpp",
"cstdio": "c"
}
}

21
CMakeLists.txt Normal file
View File

@ -0,0 +1,21 @@
cmake_minimum_required(VERSION 3.0)
add_application(Dialog
main.c
dialog.c
dialog.r
dukbridge.c
duktape.c
)
target_link_libraries(Dialog -lm)
# Enable -ffunction-sections and -gc-sections to make the app as small as possible
# On 68K, also enable --mac-single to build it as a single-segment app (so that this code path doesn't rot)
set_target_properties(Dialog PROPERTIES COMPILE_OPTIONS -ffunction-sections)
if(CMAKE_SYSTEM_NAME MATCHES Retro68)
set_target_properties(Dialog PROPERTIES LINK_FLAGS "-Wl,-gc-sections -Wl,--mac-single")
else()
set_target_properties(Dialog PROPERTIES LINK_FLAGS "-Wl,-gc-sections")
endif()

8
Makefile Normal file
View File

@ -0,0 +1,8 @@
all:
node makeResource.js > dialog.r
rm -Rf build
mkdir build
cd build && cmake .. -DCMAKE_TOOLCHAIN_FILE=~/github/R68/toolchain/m68k-apple-macos/cmake/retro68.toolchain.cmake
cd build && make

40
README.md Normal file
View File

@ -0,0 +1,40 @@
Macintosh fake-react native
===
This is a porting of my afternoon experiment to the Macintosh platform
It's based on the work I've done for Windows 3.11 for [this twitter thread](twitter.com/kentaromiura/status/1216742960408055809)
Requirements:
===
You need a build of https://github.com/autc04/Retro68, that includes the Apple Universal Interfaces,
to get those download the image from one of the links, open it in Basilisk and then copy those file back to windows: I used HVFExplorer and I copied them w/out the resource partition, seems to work.
About basilisk, I'm using the windows build, in order to enable the sharing between windows and basilisk I had to add `ignoresegv true` to the preference file as it keeps crashing otherwise in Windows 10.
To simplify this step you can find a pce/macplus image [linked in this article](http://www.toughdev.com/content/2018/12/developing-68k-mac-apps-with-codelite-ide-retro68-and-pce-macplus-emulator/)
I personally did not try it but should work fine, but probably you'll need to pull the latest changes from git (recursively).
I used WSL2, you can get this to work fine with WSL1 probably as long as you use a windows shared folder mounted on your linux for using the `launchAPPL` utility, otherwise you'll have to use HVFExplorer to copy the executable to the mac image.
If you use the launchAPPL I suggest to make an alias like
```
alias launchAPPL='/path/to/Retro68/build/build-host/LaunchAPPL/Client/LaunchAPPL -e shared --shared-directory /mnt/c/share'
```
If you use other emulators such as minivmac2 you can probably use the network share, it doesn't work on Basilisk though.
Ensure the node dependencies are installed by running yarn.
I wrote a script to create and embed the sources as mac resources inside the executable resource partition,
node wasn't strictly necessary for this, but I still needed babel to convert the JSX to ES3 that can be run in duktape, so that's why it's a nodejs file.
In the Makefile you'll need to edit the path to your Retro68 build, just change the value of `CMAKE_TOOLCHAIN_FILE` to the appropriate path.
How does this work?
===
The `main.c` is just a simple barebone program that initialize everything needed by the mac to start up an application, then it loads [duktape](https://duktape.org/) a real stack based JS engine that can run the code that babel outputs; the dukbridge file creates 2 duktape native functions that will we use to bridge the JS world to the native world.
First though we load the _app/fakeReact.js_ that creates some globals that we need, `React` with its 2 functions `createElement` and `mount`, plus a `MessageBox` and a `Dialog` _native_ components (you might notice that will call some weird `React.duktape.` methods, explained later), and two implementation specific things, an empty object property named `duktape`, under the `React` global to avoid too much pollution, and a `native` property that simply maps all the native components, this is used by `React.mount` to know if it needs to call the _native_ function or continue rendering; a little note here is that I didn't implement a fully descendant reconciler, it only supports 2 levels, the one needed for the demo, sorry!
Anyway, at this point the `populateCtx` in _dukbridge.c_ runs and connects the JS world to the native world by adding the `showDialog` and `showMessageBox` native functions to `React.duktape`.
At this point the main setup is done, so the _app/index.js_ is loaded first and the `main` function is mounted.

48
app/fakeReact.js Normal file
View File

@ -0,0 +1,48 @@
(function(global){
global.Dialog = function(descriptor){
var message = descriptor.children;
var title = descriptor.properties.title;
var onYes = descriptor.properties.onYes;
var onNo = descriptor.properties.onNo;
global.React.duktape.showDialog(title, message, onYes, onNo);
};
global.MessageBox = function(descriptor) {
var message = descriptor.children;
global.React.duktape.showMessageBox(message);
return 0;
};
global.React = {
native: {
'Dialog': Dialog,
'MessageBox': MessageBox
},
duktape: {},
createElement: function (component, properties, children) {
return {
component: component,
properties: properties,
children: children
}
},
mount: function(root) {
var handled;
Object.keys(global.React.native).forEach(function (component) {
if(global.React.native[component] == root.component) {
handled = function workaroundForBug() {root.component(root)};
}
});
if (handled) handled();
if (!handled) {
var newRoot = root.component(
Object.assign(
{
children: root.children
},
root.properties
)
);
newRoot.component(newRoot);
}
}
};
})(new Function('return this')());

29
app/index.js Normal file
View File

@ -0,0 +1,29 @@
let showMessageBox = message => {
React.mount(<MessageBox>{message}</MessageBox>);
}
let nop = () => {};
let YesNoDialog = ({
onYes = nop,
onNo = nop,
message = '',
title = ''
}) => (
<Dialog
title={title}
onYes={onYes}
onNo={onNo}
>
{message}
</Dialog>
);
let main = () => (
<YesNoDialog
title={'Attention!'}
onYes={() => showMessageBox('Ok, bye then!')}
onNo={() => showMessageBox('Alas there is not much more to do here!')}
message={'Are you sure you want to quit?'}
/>
);

83
dialog.c Normal file
View File

@ -0,0 +1,83 @@
#include <Quickdraw.h>
#include <Dialogs.h>
#include <Fonts.h>
#include <stdbool.h>
#include "types.h"
#include "utils.h"
#ifndef TARGET_API_MAC_CARBON
/* NOTE: this is checking whether the Dialogs.h we use *knows* about Carbon,
not whether we are actually compiling for Cabon.
If Dialogs.h is older, we add a define to be able to use the new name
for NewUserItemUPP, which used to be NewUserItemProc. */
#define NewUserItemUPP NewUserItemProc
#endif
// define position in resource dialog.r (128)
#define MessageBoxOK 1
#define MessageBoxMessage 2
#define MessageBoxCancel 3
#define MessageBoxTitle 4
#define AlertOK 1
#define AlertMessage 2
bool ShowAlert(char *message) {
DialogItemType type;
Handle controlHandle;
Rect box;
DialogPtr adlg = GetNewDialog(129,0,(WindowPtr)-1);
InitCursor();
{
GetDialogItem(adlg, AlertMessage, &type, &controlHandle, &box);
PString _message = toPascal(message);
SetDialogItemText(controlHandle, toConstStr255Param(_message));
}
ShowWindow(adlg);
short itemHit = NULL;
while (itemHit != AlertOK) {
ModalDialog(NULL, &itemHit);
}
DisposeDialog(adlg);
FreeDialog(129);
return true;
}
bool ShowDialog(char *message, char *title, char *ok, char *cancel) {
DialogItemType type;
Handle controlHandle;
Rect box;
DialogPtr dlg = GetNewDialog(128,0,(WindowPtr)-1);
InitCursor();
{
GetDialogItem(dlg, MessageBoxMessage, &type, &controlHandle, &box);
PString _message = toPascal(message);
SetDialogItemText(controlHandle, toConstStr255Param(_message));
GetDialogItem(dlg, MessageBoxTitle, &type, &controlHandle, &box);
PString _title = toPascal(title);
SetDialogItemText(controlHandle, toConstStr255Param(_title));
GetDialogItem(dlg, MessageBoxOK, &type, &controlHandle, &box);
PString _ok = toPascal(ok);
SetControlTitle(controlHandle, toConstStr255Param(_ok));
GetDialogItem(dlg, MessageBoxCancel, &type, &controlHandle, &box);
PString _cancel = toPascal(cancel);
SetControlTitle(controlHandle, toConstStr255Param(_cancel));
}
ShowWindow(dlg);
short itemHit = NULL;
while (itemHit != MessageBoxOK && itemHit != MessageBoxCancel) {
ModalDialog(NULL, &itemHit);
}
DisposeDialog(dlg);
return itemHit == MessageBoxOK;
}

8
dialog.h Normal file
View File

@ -0,0 +1,8 @@
#ifndef DIALOG_H
#define DIALOG_H
#include <stdbool.h>
bool ShowAlert(char *message);
bool ShowDialog(char *message, char *title, char *ok, char *cancel);
#endif

31
dialog.tpl Normal file
View File

@ -0,0 +1,31 @@
data 'DLOG' (128) {
$"0064 0051 012A 01B3 0003 0100 0100 0000" /*
.d.Q.*.>........ */
$"0000 0080 0000 280A" /* ...<2E>..(. */
};
data 'DLOG' (129) {
$"0028 0028 00F0 0118 0001 0100 0100 0000" /* .(.(. ..........
*/
$"0000 0081 0000 280A" /* ...<2E>..(. */
};
data 'DITL' (128) {
$"0003 0000 0000 00AA 0122 00BE 015C 0402" /* ....... .".<2E>.\..
*/
$"5E31 0000 0000 0028 000A 00A5 0159 8802" /*
^1.....(...<2E>.Y<>. */
$"5E30 0000 0000 00AA 000A 00BE 0044 0402" /* ^0.....
...<2E>.D.. */
$"5E32 0000 0000 000A 0073 001A 00EF 8805" /*
^2.......s...O<>. */
$"5469 746C 6500" /* Title. */
};
data 'DITL' (129) {
$"0001 0000 0000 00A0 005B 00B4 0095 0402" /* .......+.[.<2E>.<2E>..
*/
$"4F4B 0000 0000 001E 0014 008F 00DD 8807" /*
OK.........<2E>.><3E>. */
$"4D65 7373 6167 6500" /* Message. */
};

3779
duk_config.h Executable file

File diff suppressed because it is too large Load Diff

1911
duk_source_meta.json Executable file

File diff suppressed because it is too large Load Diff

45
dukbridge.c Normal file
View File

@ -0,0 +1,45 @@
// This file is almost the same used for windows 3.1 (save for LPCStrings and minor things)
#include "dukbridge.h"
#include "dialog.h"
void populateCtx(duk_context *ctx) {
duk_get_global_string(ctx, "React"); //0
duk_get_prop_string(ctx, -1, "duktape"); //1
duk_push_c_lightfunc(ctx, DUK_DIALOG, 4,4,0); //2
duk_put_prop_string(ctx, -2, "showDialog");
duk_push_c_lightfunc(ctx, DUK_MessageBox, 1,1,0);
duk_put_prop_string(ctx, -2, "showMessageBox");
}
void emptyStack(duk_context *ctx) {
duk_idx_t idx_top;
do {
idx_top = duk_get_top_index(ctx);
if (idx_top != DUK_INVALID_INDEX) duk_pop(ctx);
} while(idx_top != DUK_INVALID_INDEX);
}
duk_ret_t DUK_DIALOG(duk_context *ctx) {
duk_idx_t top = duk_get_top(ctx);
char* title = duk_safe_to_string(ctx, 0);
char* message = duk_safe_to_string(ctx, 1);
int ok = ShowDialog(message, title, "Yes", "No");
if (ok) {
duk_dup(ctx, 2);
} else {
duk_dup(ctx, 3);
}
duk_call(ctx, 0);
duk_pop_n(ctx, 5);
return 0;
}
duk_ret_t DUK_MessageBox(duk_context *ctx) {
const char* message = duk_safe_to_string(ctx, 0);
ShowAlert(message);
duk_pop(ctx);
return 0;
}

9
dukbridge.h Normal file
View File

@ -0,0 +1,9 @@
#ifndef DUKBRIDGE_H
#define DUKBRIDGE_H
#include "duktape.h"
duk_ret_t DUK_DIALOG(duk_context *ctx);
duk_ret_t DUK_MessageBox(duk_context *ctx);
void emptyStack(duk_context *ctx);
void populateCtx(duk_context *ctx);
#endif

99755
duktape.c Executable file

File diff suppressed because it is too large Load Diff

1450
duktape.h Executable file

File diff suppressed because it is too large Load Diff

72
main.c Normal file
View File

@ -0,0 +1,72 @@
#include <Quickdraw.h>
#include <Dialogs.h>
#include <Fonts.h>
#include <stdbool.h>
#include <Resources.h>
// This is probably not needed to save space,
// in case you encounter any issues aliasing builtins remove this.
#define DUK_USE_LIGHTFUNC_BUILTINS
#include "duktape.h"
#include "dukbridge.h"
#include "dialog.h"
void Initialize();
void MainLoop();
void Terminate();
int main()
{
Initialize();
MainLoop();
Terminate();
return 0;
}
void Initialize()
{
#if !TARGET_API_MAC_CARBON
InitGraf(&qd.thePort);
InitFonts();
InitWindows();
InitMenus();
TEInit();
InitDialogs(NULL);
#endif
}
char * readFromResource(int resourceId) {
Handle hnd = GetResource('TEXT', resourceId);
long size = GetResourceSizeOnDisk(hnd);
char * out = malloc(size + 1);
ReadPartialResource(hnd, 0, out, size);
out[size] = '\0';
return out;
}
void MainLoop()
{
char * fakeReact = readFromResource(129); // app/fakeReact.js
duk_context *ctx = duk_create_heap_default();
duk_eval_string(ctx, fakeReact);
free(fakeReact);
emptyStack(ctx);
populateCtx(ctx);
emptyStack(ctx);
char * source = readFromResource(128); // app/index.js
duk_eval_string(ctx, source);
free(source);
emptyStack(ctx);
duk_eval_string(ctx, "React.mount(main())");
emptyStack(ctx);
}
void Terminate()
{
ExitToShell();
}

59
makeResource.js Normal file
View File

@ -0,0 +1,59 @@
const fs = require('fs');
const babel = require('@babel/core'),
presetEnv = require('@babel/preset-env'),
presetReact = require('@babel/preset-react'),
transformClasses = require('@babel/plugin-transform-classes');
const input = fs.readFileSync('./app/index.js', 'utf8');
const fakeReact = fs.readFileSync('./app/fakeReact.js', 'utf8');
const asResource = (input, id = 128) => {
const toHex = input => {
const result = [];
let next = input;
while(next.length) {
const next16 = next
.slice(0,16)
.padEnd(16, '\0')
.split('')
.map(x => x.charCodeAt(0).toString(16).padStart(2, '0'));
result.push(` $"${next16[0]}${next16[1]} ${next16[2]}${next16[3]} ${next16[4]}${next16[5]} ${next16[6]}${next16[7]} ${next16[8]}${next16[9]} ${next16[10]}${next16[11]} ${next16[12]}${next16[13]} ${next16[14]}${next16[15]}"`)
next = next.slice(16);
}
return result.join('\n');
}
return `data 'TEXT' (${id}) {
${toHex(input)}
};`
}
const main = asResource(babel.transform(input, {
filename: 'index.js',
presets: [[presetEnv, {
"targets": {
"browsers": ["ie < 8"]
}
}],presetReact],
plugins: [transformClasses],
retainLines: true
}).code.replace('"use strict";', ''));
const react = asResource(babel.transform(fakeReact, {
filename: 'react.js',
presets: [[presetEnv, {
"targets": {
"browsers": ["ie < 8"]
}
}],presetReact],
plugins: [transformClasses],
retainLines: true
}).code.replace('"use strict";', ''), 129);
const template = fs.readFileSync('./dialog.tpl');
console.log(`${template}
${main}
${react}`);

13
package.json Normal file
View File

@ -0,0 +1,13 @@
{
"name": "NativeReactLikeMacintosh",
"version": "1.0.0",
"main": "index.js",
"author": "Cristian Carlesso <@kentaromiura>",
"license": "MIT",
"dependencies": {
"@babel/core": "^7.9.6",
"@babel/plugin-transform-classes": "^7.9.5",
"@babel/preset-env": "^7.9.6",
"@babel/preset-react": "^7.9.4"
}
}

8
types.h Normal file
View File

@ -0,0 +1,8 @@
#ifndef __TYPES_H
#define __TYPES_H
typedef struct PascalString {
unsigned char len;
char content[255];
} PString;
#endif

21
utils.h Normal file
View File

@ -0,0 +1,21 @@
#include "types.h"
#include <Dialogs.h>
#include <string.h>
#ifndef UTILS_H
#define UTILS_H
#define toConstStr255Param(var) (ConstStr255Param) &(var)
// Probably better to return a malloc'd struct for performance,
// for now this is fine.
PString toPascal(char * str) {
PString pStr;
unsigned char len = strlen(str);
if (len > 255) len = 255;
strncpy(pStr.content, str, len);
pStr.len = len;
return pStr;
}
#endif

1191
yarn.lock Normal file

File diff suppressed because it is too large Load Diff