グレンジ Advent Calendar 2018 21日目の記事を担当しているMAA_と申します。
クライアントサイドのエンジニアをやっています。
#◆はじめに
この記事では、C++ネイティブコードとの連携を含むCEPの開発方法を紹介します。
CEPとは、PhotoShopなどのAdobeCCアプリケーションの拡張機能を作成するためのプラットフォームのことです。
Adobeサイトを引用すると
Adobe Common Extensibility Platform(CEP)は、HTML5、CSS3、およびJavaScriptの標準的なWeb技術を使用して、Adobe CCアプリケーションを拡張するエクステンションを開発することが出来ます。
とあります。
大雑把に言うとGUIはHTMLで書いて、イベント処理などをJavaScriptで書くわけですが、
・公開したくないロジックを含んでいるので主処理はC++で書きたい
・既存のC++ネイティブライブラリの機能を使いたい
・重い処理を高速化したい
などの事情がある場合、CEPとC++ネイティブコードとの連携が必要になります。
CEPについてさらに詳しくは下記リンクをご覧ください。
Adobe Common Extensibility Platform(CEPエクステンション)について
なおこの記事で筆者が使用している環境は以下の通りです。
・Windows10
・PhotoShopCC2018
・VisualStudio Community 2017
この記事で使用したプログラム一式が以下のZIPに格納されています。
helloworld.zip
ZIPに格納されたPhotoShop拡張機能を使用するには本記事にも書いた通り準備がいくつか必要になります。 詳しくはZIPに格納されているreadme.txtをご覧ください。
#◆準備
この記事では下記3つのアプリを使用します。
・PhotoShopCC2018
・VisualStudio(Community 2017)
・brackets(1.13)
PhotoShopとVisualStudioのインストール方法や使い方などについてはこの記事の主旨からずれるので特に触れません。
##bracketsのインストール
bracketsは、CEPプロジェクトの新規作成、編集などに使用するAdobe製のテキストエディタです。
###本体のインストール
公式サイトでインストーラーをダウンロードしてあとはインストーラーの指示に従うだけです。
http://brackets.io/
###拡張機能のインストール
メニュー「ファイル>拡張機能マネージャー」で拡張機能マネージャーを開きます。
拡張機能マネージャーダイアログの「入手可能」タブを選択し、右上の検索テキストボックスに「Creative Cloud Extension Builder」と入力してください。
おそらく候補がいくつか表示されると思いますが、この記事では画像赤枠で囲んだ項目をインストールしてください。
もし一覧にこの拡張機能が表示されない場合は、拡張マネージャー下部の「URLからインストール...」を選択して直接下記URLを指定してインストールしてください。
https://github.com/davidderaedt/CC-Extension-Builder-for-Brackets
メニュー右端に「CC Extension Builder」が追加されていれば拡張機能のインストールは成功です。
###デバッグモードを有効にする
この作業はレジストリの編集を含んでいるので慎重に行ってください。(間違っても筆者は責任は一切取れませんのであしからずご了承ください)
・メニュー「CC Extension Builder>Enable Debug Mode」を選択します。
・次にレジストリエディタ(regedit)を起動します。regeditは、タスクバーのCortanaに「regedit」を入力するかコマンドプロンプトを起動してregeditを入力して実行します。
・レジストリキー「HKEY_CURRENT_USER\Software\Adobe\CSXS.8」を開きます。「CSXS.8」の部分はPhotoShopCC2018以外の場合は微妙に違うと思いますので、使用しているPhotoShopバージョンに合わせてください。
・上記キーに新規文字列値「PlayerDebugMode」を追加して値を「1」に設定します。
#◆HelloWorld的なものをとりあえず作ってみる
以上で作業準備は整ったので、まずはC++ネイティブコードとの連携は置いておいて早速CEPを使ったPhotoShop拡張の簡単なサンプルを作成してみます。
##Extensionを新規作成
bracketsを開きメニュー「CC Extension Builder>New Creative Cloud Extension」を選択します。
ここで拡張機能の名前(ExtensionName)やユニークID(UniqueID)などを指定できますが、今回はそのまま右下の「Create Extension」ボタンを押下します。
これで拡張機能に必要なHTMLやJavaScriptが一通り作成されました。
主なファイル構成は以下の通り。
・css:スタイルシート
・CSXS:manifest.xmlに拡張機能の構成などを定義します。
・js:JavaScriptが格納されています。主にHTML要素のイベントを処理し対応するCEPの拡張スクリプトを呼び出します。
・jsx:CEPの拡張スクリプトが格納されています。基本的にAdobe拡張機能の主処理はこちらのスクリプトに記述します。
(注意)特にJSXのスクリプトに日本語コメントがあると不可解な挙動につながる場合が多々ありました。極力日本語コメントは避けましょう。
なお、これらのファイルは、「(ユーザプライベートフォルダ)/AppData/Roaming/Adobe/CEP/extensions」以下に生成されます。
(例:C:/Users/user1/AppData/Roaming/Adobe/CEP/extensions/com.example.helloworld)
##manifest.xmlの編集
Brackets左端に表示されたツリービューからCSXS/manifest.xmlを開きます。
manifest.xmlには、
・対応するAdobeアプリのバージョン
・拡張機能パネルの名前、サイズ
・メインのCEP拡張スクリプトパス
などが定義されています。
「対応するAdobeアプリのバージョン」で実際に拡張機能を使用したいAdobeアプリのバージョンを含むように指定してやる必要があります。筆者のインストールしたBracketsではデフォルトでは以下のようにPhotoShopバージョンが設定されていました。
<!-- Photoshop -->
<Host Name="PHXS" Version="[15.0,15.9]" />
<Host Name="PHSP" Version="[15.0,15.9]" />
この指定ならバージョン15.0~15.9のPhotoShopに対応しているということになります。筆者の使用しているPhotoShopのバージョンは「19.1.6」なのでこのままだとこの拡張機能は有効にはなりません。そこで以下のように変更します。
<!-- Photoshop -->
<Host Name="PHXS" Version="[15.0,19.9]" />
<Host Name="PHSP" Version="[15.0,19.9]" />
アプリバージョンは、例えばPhotoShopCC2018の場合ならメニュー「ヘルプ>システム情報...」で確認することができます。
##拡張機能を実行してみる
ここまでに最低限の作業は完了しているので次は実際にPhotoShopで拡張機能を実行してみます。
まずはPhotoShopを起動してください。
メニュー「ウィンドウ>エクステンション」で使用可能な拡張機能が表示されるので今回追加した「Hello World」が一覧に含まれていることを確認してください。
エクステンションの中から「Hello World」を選択します。
すると下記画像のように今回追加した拡張機能専用のパネルが表示されるはずです。
「Call ExtendScrip」ボタンを押下して下記のメッセージがポップアップしたら拡張機能の実装は成功です。
以下、コードの内容を要点のみ説明します。
・main.js
ボタンクリックイベントの処理を行います。「#btn_test」というIDを持つボタンをクリックされたら「sayHello」という関数を呼び出すようになっています。
function init() {
themeManager.init();
$("#btn_test").click(function () {
csInterface.evalScript('sayHello()');
});
}
・hostscript.jsx
必要に応じて拡張機能を記述します。ここでは「hello from ExtendScript」というメッセージをポップアップ表示しています。
function sayHello(){
alert("hello from ExtendScript");
}
#◆C++ネイティブコードとの連携
さてここからが本題。先ほどのスクリプトとC++ネイティブコードを連携させてみます。
とりあえず内容としてはここまでのサンプルで「hello from ExtendScript」と表示していたメッセージの文言をDLLから取得することにします。
##VisualStudioプロジェクト作成
CEPを紹介する記事では、一般的にはCEPのコード編集には前述のBracketsを使うように勧められています。
ただ今回のようにC++ネイティブコードとの連携を実装するケースについては特にですが、筆者はCEPコードもVisualStudioで管理、編集することをお勧めします。
理由は
・CEP&C++ネイティブコードのデバッグがしやすい
・スクリプト側のコードも定義への移動や入力補完がかなり効く
からです。
この記事ではCEPコードも含め全ての関連コードをVisualStudioで管理、編集する前提で記述していきます。
いきなり話がそれてしまいましたが、今回作成するプロジェクトは2つあります。
・CEP管理用プロジェクト
・C++ネイティブコードのDLLプロジェクト
###CEP管理用プロジェクト作成
VisualStudioを開き、メニュー「ファイル>新規作成>プロジェクト」を選択。
カテゴリ「VisualC++>全般>空のプロジェクト」を選択。
名前は「HelloCEP」、場所は「(ユーザプライベートフォルダ)/AppData/Roaming/Adobe/CEP/extensions/com.example.helloworld/VS」としました。
プロジェクトを作成したらCEPのファイルを一通りこのプロジェクトに追加してしまいます。
###C++ネイティブコードのDLLプロジェクト作成
続いてこのソリューションに本題のネイティブコードを実装したDLLビルド用のプロジェクトを追加します。
メニュー「ファイル>追加>新しいプロジェクト」を選択します。
カテゴリ「VisualC++>Windowsデスクトップ>ダイナミックリンクライブラリ(DLL)」を選択。
名前は「HelloWorld」、場所は「(ユーザプライベートフォルダ)/AppData/Roaming/Adobe/CEP/extensions/com.example.helloworld/VS/HelloCEP/HelloWorld」としました。
##C++ネイティブコード編集
ネイティブコードは、HelloWorld.cppに全て記述しました。
[HelloWorld.cpp]
#include "photoshop/SoSharedLibDefs.h"
#include <stdlib.h>
#include <stdio.h>
#include <string>
#define EXPORT __declspec(dllexport)
namespace {
/// この拡張機能固有のエクスポート関数名定義
char EXTENSION_FUNCTIONS[] = {
"extGetAlertMessageDefault," /// デフォルトメッセージ取得
"extGetAlertMessage_u," /// 番号に対応するメッセージ取得
};
constexpr long HELLO_WORLD_VERSION = 1;
} // namespace
////////////////////////
// 必須のエクスポート関数
extern "C" {
/// バージョン取得
EXPORT long ESGetVersion() {
return HELLO_WORLD_VERSION;
}
/// 拡張機能を初期化
EXPORT char* ESInitialize(TaggedData*, long) {
return EXTENSION_FUNCTIONS;
}
/// 拡張機能を破棄
EXPORT void ESTerminate() {
}
/// メモリアロケート
EXPORT void* ESMallocMem(size_t size) {
void* p = malloc(size);
return p;
}
/// メモリ解放
EXPORT void ESFreeMem(void* p) {
if (p != nullptr) {
free(p);
}
}
} // 必須のエクスポート関数
////////////////////////
// この拡張機能固有のエクスポート関数
extern "C" {
/// デフォルトメッセージ取得
EXPORT long extGetAlertMessageDefault(TaggedData* inputData, long inputDataCount, TaggedData* outputData) {
const char* message = "hello from ExtendScript.";
char* str = nullptr;
{
const auto length = strlen(message) + 1;
str = (char*)malloc(length);
strcpy_s(str, length, message);
}
outputData->data.string = str;
outputData->type = kTypeString;
return kESErrOK;
}
/// 番号に対応するメッセージ取得
EXPORT long extGetAlertMessage(TaggedData* inputData, long inputDataCount, TaggedData* outputData) {
const char* messages[] = {
"Hello CEP.",
"Hello World.",
"Hello Native Extension.",
};
if (inputDataCount < 1) {
return kESErrSyntax;
}
char* str = nullptr;
{
const char* message = messages[inputData[0].data.intval];
const auto length = strlen(message) + 1;
str = (char*)malloc(length);
strcpy_s(str, length, message);
}
outputData->data.string = str;
outputData->type = kTypeString;
return kESErrOK;
}
} // この拡張機能固有のエクスポート関数
###SoSharedLibDefs.h
必須ヘッダ。必ずincludeします。
###char EXTENSION_FUNCTIONS[]
この拡張機能固有のエクスポート関数の名前を定義します。
関数が複数ある場合はカンマで区切って必要なだけ定義します。
特殊なのは、関数名末尾に付加する文字列で引数型リストを指定するところです。
基本的な書式は「関数名_引数型リスト」ですが、省略して関数名だけでも定義は可能です。
その場合、全ての引数は文字列型として扱われます。
例えば、「"funcA_s,"」と記述した場合、
・エクスポート関数名:funcA
・引数個数:1個
・第1引数:文字列値
となります。この定義は「"funcA,"」と等価です。
引数型は、
・u: 整数値(int)
・s:文字列
・f:実数値(double)
となるようです。
引数が複数ある場合は「"funcName_uusf"」のように必要なだけ型を並べて定義します。
関数名と引数型リストを区切る以外の目的でアンダースコアを関数名定義に入れないように注意してください。
例えば、「xxx_sss_uu」という関数を定義した場合、xxxに続くアンダースコアの直後を引数型リストと解釈されてしまいます。「じゃあ末尾からアンダースコアを検索しろよ」と思わないでもないですが、引数型リストの省略を許容していることを考えると先頭から検索しても末尾から検索しても見つかったアンダースコアが名前の一部なのか関数名と引数型リストを区切る意味のアンダースコアかは判断しようがないので結局は関数名と引数型リスト区切り以外のアンダースコアを使わないのが正しいようです。
###必須のエクスポート関数
Adobeのフレームワークから呼ばれる初期化処理(ESInitialize)、破棄処理(ESTerminate)などいくつかの関数を必ず実装します。
###この拡張機能固有のエクスポート関数
スクリプトから実際に呼ばれる関数を実装します。
スクリプトからのパラメータ値は第1パラメータ(TaggedData* inputData)から、パラメータの個数は第2パラメータ(long inputDataCount)から取得できます。
この関数からの結果は、第3パラメータ(TaggedData* outputData)にセットして返します。
この関数自体が成功したか失敗したかのステータスは戻り値として返します。
ここではスクリプトで表示したいメッセージ文字列を返すのでoutputData->data.stringに文字列をセットしています。
##スクリプトを修正
ネイティブコードにextGetAlertMessageDefault()とextGetAlertMessage()の2つの関数を実装したのでこれを呼び出すようにスクリプト側も修正します。
###Dll読み込み
[main.js]
CEP拡張スクリプト側の関数「initExtension()」を呼び出します。この際、CEPのルートディレクトリのパスを渡します。
var extensionDir = csInterface.getSystemPath(SystemPath.EXTENSION);
csInterface.evalScript('initExtension("' + extensionDir + '")');
[hostscript.jsx]
ExternalObjectオブジェクトを生成します。Dllへのパスを渡します。
var helloWorldDll;
function initExtension(extensionDir) {
try {
helloWorldDll = new ExternalObject("lib:" + extensionDir + "/Bin/HelloWorld.dll");
} catch (e) {
alert("exception: " + e);
}
}
###拡張関数呼び出し
ボタンを4つ配置しました。
それぞれのボタンに対応したメッセージが表示されたら成功です。
[main.js]
ボタンに対応するCEP拡張スクリプト側の関数を呼び出します。
$("#btn_test").click(function () {
csInterface.evalScript('showAlertMessageDefault()');
});
$("#btn_test2_1").click(function () {
csInterface.evalScript('showAlertMessage(0)');
});
$("#btn_test2_2").click(function () {
csInterface.evalScript('showAlertMessage(1)');
});
$("#btn_test2_3").click(function () {
csInterface.evalScript('showAlertMessage(2)');
});
[hostscript.jsx]
Dll側の関数「extGetAlertMessageDefault」または「extGetAlertMessage(index)」を呼び出します。
何れの関数も結果としてメッセージ文字列を返します。
function showAlertMessageDefault() {
// Acquire default message
var message = helloWorldDll.extGetAlertMessageDefault();
alert(message);
}
function showAlertMessage(index) {
// Acquire message corresponding to number (index)
var message = helloWorldDll.extGetAlertMessage(index);
alert(message);
}
##デバッグ実行
VisualStudioからPhotoShopを実行します。
こうすることでDllのコードにブレイクポイントを設定するなど詳細なデバッグが可能になります。
###プロジェクト「HelloWorld」をアクティブプロジェクトにする
###デバッグで実行するコマンドを設定する
・HelloWorldプロジェクトのプロパティを表示します。
・構成プロパティ>デバッグの「コマンド」にPhotoShop実行形式へのパスを入力します。
筆者の場合は、C:\Program Files\Adobe\Adobe Photoshop CC 2018\Photoshop.exeです。
###デバッガからPhotoShopを起動
デバッグ実行するとPhotoShopが起動してDllが自動的にデバッガにアタッチされるはずです。
###PhotoShopメニューから「HelloWorld」エクステンションを開く
各ボタンを押して対応するメッセージが表示されることを確認できれば拡張機能は全て完成です。
・DefaultMessageボタン:"hello from ExtendScript."
・Message#1ボタン:"Hello CEP."
・Message#2ボタン:"Hello World."
・Message#3ボタン:"Hello Native Extension."
#◆最後に
最後までお付き合いいただきまことにありがとうございます。
筆者は、普段主にC++またはC#で開発をしています。また、PhotoShopも普段はほとんど使うことはありません。そういった人間にとってCEPの開発は1日1ハマりと言っても大袈裟ではないくらいハマりの連続でした。
本当は、今回はそこらへんのハマりと回避のノウハウとか配布パッケージの作り方とかも書きたかった(特にハマりポイントについて)のですが、思った以上に記事が長くなってしまい書ききれませんでした。
そこらへんは今後新たな記事として続きを書くつもりなのでよかったらそちらもご覧ください。